¿Por qué es C la lingua franca de la programación?
Publicado el 1 de febrero de 2022
La comunicación entre lenguajes de programación es un problema difícil, y uno para el que todavía no existe una solución satisfactoria, y desde luego no una solución universal. Las bibliotecas se diseñan para un lenguaje de programación en concreto, y usarlas desde otros es difícil. Los problemas van desde los técnicos, llamar a código escrito en otro lenguaje de programación es difícil y a veces hasta imposible depende de cómo de compatibles sean dos lenguajes, a estilísticos. Las bibliotecas se diseñan para explotar las propiedades y costumbres de un lenguaje en concreto, que pueden no traducirse correctamente a otros. Trabajar con mónadas es fácil en Haskell, pero muy tedioso en Java. Tener pointers intrusivos en estructuras de datos es fácil en C++, pero un infierno en Rust. Podemos pensar ejemplos a cientos.
Entre todo este lío, C se ha establecido como la lingua franca de la programación. Cuando uno quiere que su biblioteca sea usable desde otro lenguaje, hace que sea usable desde C. Cuando un lenguaje quiere interactuar con otros, permite hacerlo con C. Suele ser común luego que se escriba una biblioteca por encima de la importada en C que la adapte a los usos y costumbres del lenguaje que la usa. El apaño más o menos funciona, mejor para algunos que para otros, pero suele requerir gran cantidad de trabajo por ambos lados para adaptarse a un cuello de botella tan estrecho como es la interfaz de C. Al menos resuelve los problemas técnicos.

¿Cómo hemos llegado hasta aquí?

C se creó a principios de los años 70 en los laboratorios Bell. Su desarrollo estuvo ligado al del sistema operativo Unix, que fue reescrito en C a partir de una primera versión en lenguaje de ensamblador, y que influyó notablemente en el diseño del lenguaje. Es conocido por ejemplo que elementos como structs o bitfields no existían en el diseño original y fueron añadidos a C a raíz de que fueron necesarios para escribir Unix. Sin embargo, además de preguntarse de qué manera influyó Unix a C, cabe preguntarse de qué manera influyó C a Unix.
Es innegable a estas alturas que Unix es el sistema operativo más influyente que ha existido, no sólo por su amplio uso comercial durante las últimas tres décadas del siglo XX, sino por su influencia directa en Linux, el estándar POSIX y el proyecto GNU. La llamada filosofía de Unix que guiaba el diseño de las herramientas de línea de comandos de aquel sistema operativo se coló también en las de GNU e incluso en las de Windows. Si hoy Windows tiene una terminal parecida a bash y programas como el compilador o el linker se lanzan desde ella con parámetros de consola es por influencia directa de Unix, que recordemos que era un sistema operativo pensado para un PDP-11 conectado a un teletipo. Decisiones de diseño tomadas entonces han tenido un impacto directo sobre el estado de la informática hoy en día.
pdp11
La forma en la que en Unix se lograba la comunicación entre código escrito por varias partes, por ejemplo un programa y una biblioteca, era gracias al linker. El compilador de C compila cada archivo de código a un archivo binario diferente, que recibe el nombre de objeto y la extensión .o o .obj, y es el trabajo del linker coger todos esos archivos binarios y generar un ejecutable. Entre sus funciones está la de resolver dependencias externas. Es decir, cuando un archivo hace referencia a un símbolo que no conoce pero que debería estar presente en otro archivo, el linker entra en juego y sustituye esa referencia externa por la dirección del código o memoria asociados a ese símbolo en el ejecutable final. En principio, el linker es agnóstico de lenguaje de programación. Un archivo obj contiene tan sólo código de máquina compilado, y cualquier lenguaje que pueda generar archivos obj puede interoperar con cualquier otro. En principio. Como bien dice el dicho, en teoría no hay diferencia entre la teoría y la práctica, pero en la práctica la hay.
ELF, el formato definido por Unix para los archivos binarios que contienen código ejecutable, es un buen formato para describir código escrito en C. Prácticamente toda la información que un usuario de código escrito en C puede necesitar puede obtenerse de la combinación del archivo header que describe el contenido del binario y del obj con el código. Esto no es cierto para otros lenguajes de programación, cuyo resultado puede consistir de estructuras más complejas que interfaces de función, declaración de variables globales y código compilado. Las funciones genéricas por ejemplo son imposibles de representar en un archivo obj. Sucede lo mismo con metadatos que describan las estructuras de datos que contiene una biblioteca. Usar strings para nombrar los símbolos funciona para C, donde cada nombre refiere exactamente a una única entidad en el programa, pero no es cierto para lenguajes que permiten sobrecargar funciones o separar nombres en distintos módulos o espacios e incluirlos condicionalmente. Al limitar el formato de interacción entre código escrito por partes distintas a lo que es expresable en C el linker crea una limitación artificial en la capacidad de expresión de lo que una biblioteca puede exponer a código escrito en otros lenguajes. Formatos e interfaces añadidas posteriormente como las bibliotecas o archivos (.lib, .a) y los objetos compartidos o bibliotecas dinámicas (.so, .dll), construidos sobre ELF, no hicieron sino cimentar esta hegemonía de C en el negocio de la interacción entre código de distintas partes.
Otro detalle sutil del código en C expuesto en un obj es la convención de llamada. El lenguaje de ensamblador no define cómo se llama a una función. Es más, el concepto de función no existe en el procesador, para el que todo son saltos del puntero de instrucción. Sin embargo, C define una sofisticada ceremonia que el código debe seguir para poder llamar a otra función, describiendo cómo pasar los parámetros y el valor de retorno, quién es el encargado de liberar la memoria ocupada por las variables locales de la función siendo llamada o cuáles son las pre y poscondiciones sobre los registros al llamar a la función. Diferentes lenguajes de programación pueden tener diferentes convenciones de llamada, ya que, cómo hemos explicado, son convenciones, no hay ninguna imposición real por parte del hardware. Hasta que tienen que interoperar con C. Entonces tienen que usar la convención de C, por lo que están de nuevo atados por ésta. El ejemplo más llamativo tal vez sea que la convención de llamada de C no sabe qué hacer con excepciones, por lo que una biblioteca no puede tirar excepciones a través de una frontera de interfaz de C.
Quizá la gran mentira de C sea eso de que es assembly portátil. Parece más bien que C ha modelado al lenguaje de ensamblador a su imagen y semejanza, y siendo como es un lenguaje con una capacidad de expresión tirando a escasa, eso ha atado a todos los demás en lo que a su capacidad para interactuar se refiere. Dos lenguajes distintos que ambos implementen excepciones no pueden interactuar mediante ellas porque la interacción necesariamente va a ir mediada por C, que no lo permite. Las herramientas y formatos de Unix se diseñaron con C en mente, las herramientas y formatos actuales son en gran medida descendientes de aquellas y esto impone una limitación en la capacidad de interacción entre lenguajes de programación.

El mundo que podría haber sido

Es interesante estudiar los lenguajes de programación de los 60 y 70, cuando todo era campo, ya que presentan algunas ideas realmente innovadoras debido a su poca necesidad de interactuar con lo preexistente, que era poco o nada. Un proyecto que sucedió en la misma época que C, comenzando también en 1972, y que tenía una ambición parecida fue Smalltalk. Recordado por ser uno de los primeros lenguajes orientados a objetos y por valerle a Alan Kay un premio Turing en 2003, Smalltalk ambicionaba ser mucho más. El objetivo era construir todo un sistema integrado, llamado la máquina virtual de Smalltalk, donde todos los programas vivieran simultáneamente y se comunicaran entre ellos. El editor de Smalltalk era un programa inteligente que entendía la estructura del código, facilitaba el navegar por él y era capaz de actualizarlo mientras se ejecutaba y dar estadísticas en vivo sobre el programa siendo editado. La máquina virtual era un diseño de sistema operativo radicalmente distinto a Unix. La comunicación entre los programas no sucedía en protocolos binarios sino mediante información estructurada en formatos definidos por el lenguaje, que el código receptor podía inspeccionar para actuar sobre ella. Aunque las únicas versiones que existieron de la máquina virtual fueron programas que se ejecutaban sobre otros sistemas operativos, el equipo que la desarrolló ambicionaba que algún día el ordenador fuera una máquina de Smalltalk. Otros lenguajes de programación serían posibles siempre y cuando fueran capaces de hablar los protocolos de Smalltalk, mucho más ricos que los de C, y tendrían acceso total a ese rico entorno.
Al final la historia no les dio la razón, la adopción fue lenta y terminó decayendo, y la verdadera influencia de Kay consistió en inspirar a otros lenguajes de programación y convertir la orientación a objetos en la cosa de moda del momento en los 90 y 2000. Nadie desarrolló nunca un segundo lenguaje para la máquina virtual de Smalltalk. Hubo intentos de máquina virtual más modestos y más exitosos como Java, por ejemplo, pero nadie volvió a intentar diseñar un sistema operativo con unas bases radicalmente distintas a las existentes. Al final, las máquinas de Smalltalk que ambicionaba el equipo de Palo Alto nunca llegaron a ser y los ordenadores se convirtieron en máquinas de C. Hay una cierta ironía en lo mucho que se parecen ciertas partes de Unix, las relacionadas con la interoperación de código, a una versión chapucera, limitada y construida a parches de lo que Smalltalk iba a ser.

Otros ejemplos de comunicación entre lenguajes de programación hoy en día

Mencionábamos antes la máquina virtual de Java. Creada originalmente para el lenguaje de programación Java en 1995, la máquina virtual de Java define un formato de ejecutable binario lo bastante rico como para poder implementar otros lenguajes sobre él. Un ejemplo de estos es Clojure, un lisp que se compila a este bytecode de Java. Esto significa que el código de Clojure y el escrito en Java pueden interoperar sin ningún esfuerzo, teniendo ambos acceso total a todo el código escrito en el otro. También comparten abstracciones definidas por la máquina virtual de Java como el garbage collector, las tablas de funciones virtuales o la clase de string, que son los mismos para ambos lenguajes, lo que permite a ambos lenguajes escribir interfaces muy ricas y compartir estructuras de datos complejas sin tener que crear copias redundantes que traduzcan la información del otro a un formato inteligible por el receptor.
Otro ejemplo llamativo de un lenguaje definiendo los protocolos de comunicación hasta volverse hegemónico lo hemos visto en el mundo web con JavaScript y JSON. JSON, la notación de objetos de JavaScript (JavaScript Object Notation), es un sublenguaje de JavaScript que permite describir cualquier objeto de éste siempre y cuando no contenga funciones. Es decir, cualquier estructura de datos de JavaScript, lo que no es poco. Lo mejor es que un objeto de JSON es automáticamente un objeto de JavaScript, y un objeto de JavaScript es automáticamente un objeto de JSON, lo que facilita muchísimo la interacción de programas escritos en JavaScript con interfaces HTTP basadas en JSON. Teniendo en cuenta que la mayoría de páginas web están escritas en JavaScript, que con el auge de node.js cada vez más servidores lo están también y que gracias a CEF y Electron muchos programas de escritorio empiezan a estar escritos en JavaScript, no es de extrañar que JSON domine la mayoría de interfaces REST, por encima de protocolos más eficientes y más robustos como protobuf. Lo que lleva a que incluso servidores que no están escritos en JavaScript ofrezcan su interfaz en JSON por diversas razones: porque hay buenas bibliotecas de JSON, porque es lo que los clientes esperan o simplemente porque es lo que se hace.

Conclusión

Los protocolos en amplio uso son con frecuencia fruto de contingencias históricas y del crecimiento gradual de proyectos con alcance más modesto, que van aumentando la cantidad de usuarios y desbancando a la competencia hasta convertirse en el protocolo de facto. Llega un punto en el que una infraestructura es demasiado grande como para reemplazarla y termina tocando vivir con ella. Los protocolos no son agnósticos de los lenguajes de programación en los que están escritos los programas que los hablan, y es frecuente que estos lenguajes modelen el diseño, aunque sea indirectamente, ya que modelan la forma de pensar de quienes diseñan el protocolo y los programas que lo usan. También es muy difícil ver de antemano las posibles fallas del diseño. Por lo general un diseño suele ser válido para la época en la que es desarrollado, y los diseños que se vuelven hegemónicos lo hacen porque tienen un tiempo de vigencia largo. Es al cabo de los años cuando se empiezan a encontrar las grietas, pero también es entonces cuando su uso se ha extendido lo suficiente como para ser difíciles de reemplazar.
Las plataformas sobre las que se ejecuta código, como sistemas operativos, navegadores o máquinas virtuales, tienen un gran poder a la hora de definir los protocolos por los que otros se tendrán que regir. Unix está influido por C, y los sistemas operativos actuales están influidos por Unix. La tecnología de binarios y el linker no han cambiado sustancialmente desde los 80, así que es normal que ese sea un aspecto especialmente obsoleto de los sistemas operativos actuales. La mayoría de lenguajes de programación encuentran una forma de resolverlo internamente, siendo el más común el conocido como
name mangling
. Es la interacción entre lenguajes de programación, donde no hay un protocolo de
mangling
establecido, la que paga el pato de la obsolescencia de esta tecnología.
En el tercer apartado de este texto se habla bien de Smalltalk. Se puede hablar bien de Smalltalk porque nunca llegó a ser. Si hubiera tenido el grado de adopción y distribución de C, hoy estaríamos hablando de sus problemas y de cómo su modelo de ordenador también se ha quedado obsoleto. Es interesante recordar Smalltalk para tener en mente alternativas y entender que un sistema operativo radicalmente distinto es posible, lo que resulta ser un soplo de aire fresco en un entorno en el que todos los sistemas operativos beben muchísimo de la misma raíz y se parecen mucho. Eso no significa que la solución ideal a día de hoy sea literalmente Smalltalk. Si no llegó a ser también es por algo, pero ese es otro texto.
Logo of RSS.