Esta página lleva la tesis del programa hasta el final: aprende el representante, reconoce la familia entera. El mismo problema de la clase —llamar a una función que vive fuera del lenguaje— resuelto por los primos de cada familia del Atlas, no solo por los diez lenguajes del núcleo.
Aquí hay algo que ninguna otra clase tiene: los veinte programas apuntan al mismo destino. C es el idioma franco de la interoperabilidad, y lo que cambia de un primo a otro no es el objetivo sino el puente. Léelos como veinte respuestas distintas a la misma pregunta: ¿cómo cruzo?
⚠️ Qué está verificado y qué no. Ruby, Perl y Lua se ejecutan en CI contra el mismo
casos.jsonque el núcleo, igual que las diez implementaciones de la clase (workflow Labs). Los otros 17 primos son material de lectura: su toolchain no está en el workflow, así que están escritos para ser correctos pero sin el sello de la máquina. Verificar tres de veinte no es verificarlos todos.
Casi todos los bloques suponen esta biblioteca compartida al lado, que es la función "externa" que se quiere llamar:
/* doble.c → cc -shared -fPIC doble.c -o libdoble.so */
long doble(long x) { return x * 2; }
nresultado=<2n>doble(n), que vive fuera del lenguaje anfitrión| stdin | esperado |
|---|---|
5 |
resultado=10 |
0 |
resultado=0 |
7 |
resultado=14 |
Representantes del núcleo: Python · PHP. La familia con más motivos para cruzar: son lenguajes lentos, con intérpretes escritos en C, cuyo rendimiento real depende de que las partes calientes estén del otro lado de la frontera.
# Fiddle está en la biblioteca estándar: no hace falta compilar el binding,
# pero sí que exista libdoble.so al lado, así que el puente va como
# declaración y el otro lado se simula (igual que la versión del núcleo):
# require "fiddle"
# require "fiddle/import"
# module Nativa
# extend Fiddle::Importer
# dlload "./libdoble.so"
# extern "long doble(long)"
# end
def doble(x) # simula la función que vive en C
x * 2
end
n = STDIN.gets.to_i
puts "resultado=#{doble(n)}"
# FFI::Platypus declara la firma en tiempo de ejecución; XS la compilaría.
# El módulo es del CPAN, no del núcleo, y además hace falta libdoble.so al lado,
# así que el puente va aquí como declaración y el otro lado se simula:
# use FFI::Platypus 2.00;
# my $ffi = FFI::Platypus->new(api => 2, lib => './libdoble.so');
# $ffi->attach(doble => ['long'] => 'long');
sub doble { $_[0] * 2 } # simula la función que vive en C
chomp(my $n = <STDIN>);
printf "resultado=%d\n", doble($n);
-- doble.so aquí es un módulo C escrito contra la API de pila de Lua:
-- static int l_doble(lua_State *L) {
-- lua_Integer x = luaL_checkinteger(L, 1); -- saca el argumento de la pila
-- lua_pushinteger(L, x * 2); -- deja el resultado en la pila
-- return 1; -- cuántos valores devuelve
-- }
-- Ese módulo hay que compilarlo (`gcc -shared -o doble.so ...`), así que aquí
-- el otro lado se simula, como en la versión del núcleo:
local nativa = { doble = function(x) return x * 2 end }
local n = tonumber(io.read("l"))
print("resultado=" .. nativa.doble(n))
# load llama a Doble_Init, que registra el comando con Tcl_CreateObjCommand.
load ./libdoble.so Doble
gets stdin n
puts "resultado=[doble $n]"
# .Call entra en la API C de R: doble_r recibe y devuelve SEXP, el tipo universal.
dyn.load("libdoble.so")
n <- as.integer(readLines("stdin", n = 1))
cat(sprintf("resultado=%d\n", .Call("doble_r", n)))
Qué reconocer: los cinco cargan una biblioteca por su nombre en disco y le piden un símbolo, pero
la forma del puente delata para qué se diseñó cada lenguaje. Ruby y Perl declaran la firma desde
el propio lenguaje (Fiddle, FFI::Platypus) y por eso no exigen compilar nada; Perl también
conserva el camino antiguo, XS, que sí genera y compila C. R obliga a que la función del otro lado
hable su moneda interna, el SEXP, porque todo valor de R es un objeto del intérprete.
Y luego está Lua, que es el caso más ilustrativo de toda la tanda. Lua no tiene una FFI que
imite firmas de C: tiene una pila compartida entre el intérprete y el código C. El lado C no
recibe argumentos ni devuelve un valor al estilo normal, sino que saca los argumentos de la pila
con luaL_checkinteger y empuja el resultado con lua_pushinteger, devolviendo cuántos valores
dejó. Esa disciplina de pila no es un detalle de implementación: es la razón de ser del lenguaje. Lua
nació para ser incrustado dentro de un programa en C —un motor de videojuego, un servidor web, un
editor—, y por eso su frontera con C no está en un rincón de la biblioteca estándar, sino en el
centro del diseño. Los demás primos añadieron un puente hacia C; Lua es el puente.
Representantes del núcleo: JavaScript · TypeScript.
import 'dart:ffi';
import 'dart:io';
typedef DobleC = Int64 Function(Int64); // la firma tal como la ve C
typedef DobleDart = int Function(int); // la firma tal como la ve Dart
void main() {
final lib = DynamicLibrary.open('./libdoble.so');
final doble = lib.lookupFunction<DobleC, DobleDart>('doble');
final n = int.parse(stdin.readLineSync()!.trim());
print('resultado=${doble(n)}');
}
// ActionScript 3 no tiene stdin ni FFI: el reproductor es una caja cerrada.
// En AIR el único puente a código nativo son las ANE (Adobe Native Extensions),
// que empaquetan la biblioteca aparte. Aquí solo se ilustra el cálculo.
package {
public class Ffi {
public static function doble(n:int):String {
return "resultado=" + (n * 2);
}
}
}
Qué reconocer: dart:ffi obliga a escribir la firma dos veces —una con los tipos de C
(Int64) y otra con los de Dart (int)— porque el compilador necesita saber a la vez cómo se
empaquetan los argumentos en registros y cómo se ven desde el lenguaje. Es la separación entre ABI y
tipos que la clase 157 hará explícita. ActionScript marca el contraste: un lenguaje cuya máquina
virtual fue diseñada como sandbox no puede tener FFI, porque llamar a C es precisamente lo que un
sandbox existe para impedir. En el navegador, JavaScript vive la misma restricción, y por eso su
puente moderno no es C sino WebAssembly.
Representante del núcleo: Java. Un solo mecanismo para todos los lenguajes de la plataforma: lo que sirve a Java sirve a Kotlin, Scala, Groovy y Clojure sin cambios.
// JNI: el símbolo del lado C debe llamarse Java_MainKt_doble, con esa decoración exacta.
external fun doble(n: Long): Long
fun main() {
System.loadLibrary("doble")
val n = readLine()!!.trim().toLong()
println("resultado=${doble(n)}")
}
// Foreign Function & Memory API (proyecto Panama, JEP 454, estable desde Java 22):
// llama a la libdoble.so sin escribir una sola línea de C.
import java.lang.foreign.{Arena, FunctionDescriptor, Linker, SymbolLookup, ValueLayout}
object Ffi {
def main(args: Array[String]): Unit = {
val arena = Arena.ofConfined()
val lib = SymbolLookup.libraryLookup("./libdoble.so", arena)
val doble = Linker.nativeLinker().downcallHandle(
lib.find("doble").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG))
val n = scala.io.StdIn.readLine().trim.toLong
val r = doble.invokeWithArguments(n).asInstanceOf[Long]
println(s"resultado=$r")
}
}
@Grab('net.java.dev.jna:jna:5.14.0')
import com.sun.jna.Library
import com.sun.jna.Native
interface Nativa extends Library {
long doble(long n)
}
def nativa = Native.load('doble', Nativa)
def n = System.in.newReader().readLine().trim() as long
println "resultado=${nativa.doble(n)}"
;; JNA sin declarar interfaces: el símbolo se resuelve en tiempo de ejecución.
(import '[com.sun.jna Function])
(let [doble (Function/getFunction "doble" "doble")
n (Long/parseLong (.trim (read-line)))]
(println (str "resultado=" (.invokeLong doble (to-array [n])))))
Qué reconocer: el mecanismo histórico es JNI, y su precio se ve en el comentario de Kotlin:
no basta con que exista una función doble en C, tiene que llamarse Java_MainKt_doble y aceptar
un puntero al entorno de la JVM. JNI no llama a C, obliga a escribir C para la JVM. JNA fue la
respuesta de la comunidad —declarar la interfaz en Java y que la biblioteca haga el resto—, y el
Foreign Function & Memory API (proyecto Panama, JEP 454, estable en Java 22) es la respuesta
oficial: un Linker, un FunctionDescriptor que describe la firma con ValueLayout, y una Arena
que gobierna la vida de la memoria fuera del recolector de basura. Los cuatro lenguajes usan
exactamente las mismas clases porque, en la JVM, la frontera pertenece a la plataforma y no al
lenguaje.
Representante del núcleo: C#. Un solo mecanismo, P/Invoke, para toda la
plataforma.
open System.Runtime.InteropServices
[<DllImport("doble", CallingConvention = CallingConvention.Cdecl)>]
extern int64 doble(int64 n)
[<EntryPoint>]
let main _ =
let n = int64 (stdin.ReadLine().Trim())
printfn "resultado=%d" (doble n)
0
Imports System.Runtime.InteropServices
Module Ffi
<DllImport("doble", CallingConvention:=CallingConvention.Cdecl)>
Private Function doble(n As Long) As Long
End Function
Sub Main()
Dim n = Long.Parse(Console.ReadLine().Trim())
Console.WriteLine("resultado=" & doble(n))
End Sub
End Module
Qué reconocer: el mismo atributo DllImport y la misma función declarada sin cuerpo en los
dos lenguajes: el cuerpo lo pone el enlazador en tiempo de ejecución. Esto es P/Invoke, y es el
único puente que hace falta en .NET porque el CLR es quien marshalea, no el lenguaje. Fíjate en
CallingConvention.Cdecl: la plataforma te pide declarar cómo se pasan los argumentos, no solo
de qué tipo son. Ese parámetro es el tema íntegro de la clase 157, y aquí aparece como un argumento
más de un atributo.
Representante del núcleo: C. La familia que no necesita puente, porque es la orilla a la que llegan todos los demás.
#include <iostream>
// extern "C" apaga el name mangling: sin él, el enlazador buscaría _Z5doblel.
extern "C" long doble(long);
int main() {
long n;
std::cin >> n;
std::cout << "resultado=" << doble(n) << '\n';
}
#import <Foundation/Foundation.h>
/* Objective-C ES C: la funcion se declara y se llama sin ninguna ceremonia. */
long doble(long x) { return x * 2; }
int main(void) {
@autoreleasepool {
long n;
if (scanf("%ld", &n) != 1) return 1;
printf("resultado=%ld\n", doble(n));
}
return 0;
}
Qué reconocer: aquí la palabra clave es "ninguna". Objective-C no necesita FFI hacia C porque
es C: un archivo .m puede contener funciones C, punteros C y printf sin cambiar de registro.
C++ está a un milímetro, y ese milímetro se llama name mangling: como C++ admite sobrecarga, el
compilador decora los nombres con la firma, y extern "C" es la instrucción de no decorar este. Por
eso todas las bibliotecas de C++ que quieren ser llamadas desde fuera exponen una fachada
extern "C" — y por eso todos los primos de esta página llaman a C y no a C++ directamente.
Representantes del núcleo: Go · Rust. Compilan a binario nativo, así que su frontera con C es un enlace, no una traducción.
const std = @import("std");
// @cImport lee la cabecera de C de verdad: sin bindings, sin generador, sin declarar la firma.
const c = @cImport(@cInclude("doble.h"));
pub fn main() !void {
var buf: [64]u8 = undefined;
const linea = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')).?;
const n = try std.fmt.parseInt(c_long, std.mem.trim(u8, linea, " \r"), 10);
try std.io.getStdOut().writer().print("resultado={d}\n", .{c.doble(n)});
}
import std/strutils
# importc dice "este símbolo ya existe en C"; dynlib, dónde buscarlo.
proc doble(n: clong): clong {.importc: "doble", dynlib: "libdoble.so".}
let n = stdin.readLine().strip().parseInt().clong
echo "resultado=", doble(n)
import std.stdio, std.string, std.conv;
extern (C) long doble(long n); // declarada aquí, definida en libdoble.so
void main() {
long n = readln().strip().to!long;
writefln("resultado=%d", doble(n));
}
Qué reconocer: los tres declaran la función en una sola línea, sin biblioteca intermedia, porque
comparten con C el modelo de memoria y el mismo ABI de la plataforma. Zig va un paso más allá que
cualquier otro primo de la página: @cImport lee la cabecera de C directamente y genera las
declaraciones en tiempo de compilación, y el propio compilador de Zig sabe compilar C. Donde los
demás mantienen un archivo de bindings que se desincroniza, Zig y D tratan la cabecera de C como
código fuente propio. Nim, que compila a C, lo tiene aún más fácil: importc acaba siendo una
llamada C a C.
Representante del núcleo: SQL. La familia que se queda al margen de esta clase, y conviene decirlo sin adornos.
:- initialization(main, main).
:- use_foreign_library(foreign(doble)). % el lado C llama a PL_register_foreign
main :-
read_line_to_string(user_input, Linea),
number_string(N, Linea),
doble(N, R),
format("resultado=~d~n", [R]).
% Datalog no tiene FFI ni efectos: no puede llamar a C bajo ningún mecanismo.
% Lo más cercano es derivar el resultado con una regla dentro del propio programa.
entrada(5).
resultado(R) :- entrada(N), R = N * 2.
Qué reconocer: esta familia queda fuera del tema de la clase, y por razones distintas cada
una. SWI-Prolog sí tiene interfaz con C, pero fíjate en la firma: doble(N, R) toma dos
argumentos, porque en Prolog una función no devuelve, unifica; el lado C tiene que construir
términos con PL_unify_integer en vez de retornar un long. La impedancia no es de tipos, es de
modelo de cómputo. Datalog directamente no puede: sin E/S y sin efectos, no hay nada que llamar.
Y SQL, el representante del núcleo, tampoco tiene FFI propia: las funciones definidas por el usuario
las carga el motor (CREATE FUNCTION ... LANGUAGE C en PostgreSQL), no el lenguaje. En los
declarativos, cruzar a C es una decisión del intérprete, no del programa.
Veinte lenguajes, un solo destino. Unos declaran la firma en su propio código, otros exigen escribir C a su medida, uno lee la cabecera de C tal cual, y uno —Lua— hizo de la frontera su arquitectura. Lo transferible es la pregunta que hay debajo de todos: quién describe la firma, quién reserva la memoria y quién decide cómo se pasan los argumentos. Las dos clases siguientes responden esa última parte.