Esta página lleva la tesis del programa hasta el final: aprende el representante, reconoce la familia entera. El mismo problema de la clase —decidir si dos componentes encajan a nivel de ABI— resuelto por los primos de cada familia del Atlas, no solo por los diez lenguajes del núcleo.
El programa en sí es una comparación de dos números. Lo interesante está en los comentarios: cada primo enseña qué le ocurre a una función por debajo de su nombre cuando el compilador la deja en el archivo objeto. Ese nombre decorado, esa convención de llamada y ese ancho de bits son el ABI.
⚠️ 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.
a b, el ancho de bits de cada componenteabi=compatible o abi=incompatible| stdin | esperado |
|---|---|
64 64 |
abi=compatible |
64 32 |
abi=incompatible |
32 32 |
abi=compatible |
Representantes del núcleo: Python · PHP. Ninguno de los cinco compila a código máquina, así que ninguno tiene ABI propia: la que importa es la del intérprete, que sí es un programa en C compilado con la de la plataforma.
a, b = STDIN.gets.split.map(&:to_i)
puts "abi=#{a == b ? 'compatible' : 'incompatible'}"
my ($ancho_a, $ancho_b) = split ' ', <STDIN>;
printf "abi=%s\n", $ancho_a == $ancho_b ? "compatible" : "incompatible";
local a, b = io.read("n", "n")
print("abi=" .. (a == b and "compatible" or "incompatible"))
gets stdin linea
lassign [split $linea] a b
puts "abi=[expr {$a == $b ? {compatible} : {incompatible}}]"
v <- as.integer(strsplit(trimws(readLines("stdin", n = 1)), " +")[[1]])
cat(sprintf("abi=%s\n", if (v[1] == v[2]) "compatible" else "incompatible"))
Qué reconocer: los cinco escriben la comparación sin declarar un solo tipo, y esa es justamente
la razón por la que el ABI los muerde tan fuerte cuando cruzan a C. Una firma declarada con
Fiddle, FFI::Platypus o ctypes es solo una promesa en tiempo de ejecución: nadie la
contrasta con la biblioteca real. Si prometes long donde el otro lado espera int, no hay error
de compilación —no hay compilación—, hay una lectura de basura o un fallo de segmentación. El propio
Lua lo lleva escrito en su configuración: lua_Integer es un alias definido en luaconf.h, y una
compilación con enteros de 32 bits y otra con 64 producen intérpretes con ABI incompatible entre
sí, aunque el archivo .lua sea idéntico. En esta familia el ABI no está en tu código; está en
cómo se compiló el binario que lo ejecuta.
Representantes del núcleo: JavaScript · TypeScript.
import 'dart:io';
void main() {
final v = stdin.readLineSync()!.trim().split(RegExp(r'\s+')).map(int.parse).toList();
print('abi=${v[0] == v[1] ? 'compatible' : 'incompatible'}');
}
// La AVM2 es un sandbox: no hay stdin ni símbolos nativos que enlazar.
// Aquí solo se ilustra la comparación.
package {
public class Abi {
public static function comparar(a:int, b:int):String {
return "abi=" + (a == b ? "compatible" : "incompatible");
}
}
}
Qué reconocer: Dart es el único primo de la página que le puso nombre propio al concepto:
dart:ffi incluye una clase Abi con valores como Abi.linuxX64 o Abi.windowsArm64, y tipos
como IntPtr y Size cuyo ancho no se conoce hasta saber para qué ABI se compila. Con
AbiSpecificInteger puedes declarar que un tipo mide 32 bits en un ABI y 64 en otro. Es exactamente
el problema de esta clase convertido en API. ActionScript marca el contraste opuesto: una máquina
virtual diseñada como sandbox no expone ABI alguna, porque conocer la disposición de los registros
es el primer paso para escapar de ella.
Representante del núcleo: Java. Cuatro lenguajes, un solo formato de enlace:
el del archivo .class.
fun main() {
val (a, b) = readLine()!!.trim().split(Regex("\\s+")).map { it.toInt() }
println("abi=" + if (a == b) "compatible" else "incompatible")
}
object Abi {
def main(args: Array[String]): Unit = {
val Array(a, b) = scala.io.StdIn.readLine().trim.split("\\s+").map(_.toInt)
println(s"abi=${if (a == b) "compatible" else "incompatible"}")
}
}
def (a, b) = System.in.newReader().readLine().trim().split(/\s+/)*.toInteger()
println "abi=${a == b ? 'compatible' : 'incompatible'}"
(require '[clojure.string :as str])
(let [[a b] (map parse-long (str/split (str/trim (read-line)) #"\s+"))]
(println (str "abi=" (if (= a b) "compatible" "incompatible"))))
Qué reconocer: la JVM no tiene ABI de C, y conviene decirlo así de claro. Tiene su propio
esquema de enlace, y ese esquema es más rico, no más pobre: los símbolos del .class conservan el
nombre y el tipo completos —el método doble aparece como doble con el descriptor (J)J— y quien
los resuelve no es el enlazador sino el cargador de clases, en tiempo de ejecución. Por eso en la
JVM no existe el name mangling al estilo C++: no hace falta codificar la firma en el nombre cuando
el formato ya guarda la firma aparte.
El precio se paga al cruzar. Como no hay ABI de C, hace falta una capa que la fabrique: JNI, con
sus símbolos Java_Clase_metodo y su puntero JNIEnv*, o el Foreign Function & Memory API del
proyecto Panama (JEP 454, estable desde Java 22), donde describes la firma con ValueLayout. Y ahí
aparece la trampa de esta clase: ValueLayout.JAVA_LONG mide siempre 64 bits, pero el long de
C mide 64 en Linux (modelo LP64) y 32 en Windows (modelo LLP64). El mismo archivo .class,
literalmente idéntico, es compatible en un sistema e incompatible en el otro. Un long de Java
nunca cambia de tamaño; un long de C sí.
Representante del núcleo: C#. Igual que la JVM: enlace propio, ABI de C prestada.
[<EntryPoint>]
let main _ =
let v = stdin.ReadLine().Trim().Split(' ') |> Array.map int
printfn "abi=%s" (if v.[0] = v.[1] then "compatible" else "incompatible")
0
Module Abi
Sub Main()
Dim v = Console.ReadLine().Trim().Split(" "c)
Dim a = Integer.Parse(v(0))
Dim b = Integer.Parse(v(1))
Console.WriteLine("abi=" & If(a = b, "compatible", "incompatible"))
End Sub
End Module
Qué reconocer: el CLR tampoco tiene ABI de C. Los ensamblados se enlazan por metadatos
—nombre, espacio de nombres, versión y clave pública— y la frontera con lo nativo la abre
P/Invoke, es decir el atributo DllImport. Fíjate en lo que ese atributo te obliga a declarar
además del tipo: CallingConvention. En Windows conviven dos convenciones históricas y no son
intercambiables. En cdecl, la del lenguaje C, es quien llama el que limpia la pila de
argumentos, lo que permite funciones variádicas como printf. En stdcall, la de la API Win32,
limpia la función llamada, que ahorra unas instrucciones por llamada pero exige conocer de
antemano cuántos argumentos hay. Equivocarse de convención no da un error: desequilibra la pila y el
programa muere unas cuantas llamadas después, en un sitio que no tiene nada que ver. El valor por
defecto de DllImport es Winapi, que en Windows significa StdCall —así que declarar una función
de C sin tocar ese campo es el error clásico—. En x86-64 la distinción se desvanece: Win64 define una
sola convención, y cdecl y stdcall son sinónimos. Es una herencia viva del Windows de 32 bits.
Representante del núcleo: C. La familia donde el ABI es el tema, porque es la única que exporta símbolos que todos los demás pueden ver.
#include <iostream>
// Sin extern "C" el símbolo sale decorado. La misma función se exporta como
// _Z14abi_compatibleii con GCC/Clang y como ?abi_compatible@@YA_NHH@Z con MSVC:
// el name mangling NO está estandarizado, cada compilador inventó el suyo.
extern "C" bool abi_compatible(int a, int b) { return a == b; }
int main() {
int a, b;
std::cin >> a >> b;
std::cout << "abi=" << (abi_compatible(a, b) ? "compatible" : "incompatible") << '\n';
}
#import <Foundation/Foundation.h>
/* Objective-C ES C: el simbolo sale a la tabla como _abi_compatible, sin decorar
y sin necesitar ningun puente. Los METODOS son otra historia (ver abajo). */
int abi_compatible(int a, int b) { return a == b; }
int main(void) {
@autoreleasepool {
int a, b;
if (scanf("%d %d", &a, &b) != 2) return 1;
printf("abi=%s\n", abi_compatible(a, b) ? "compatible" : "incompatible");
}
return 0;
}
Qué reconocer: este es el caso central de la clase. C++ admite sobrecarga: puedes tener
doble(int) y doble(double) a la vez. Pero el archivo objeto es una tabla plana de nombres, y en
una tabla plana no caben dos entradas iguales. La solución fue el name mangling: el compilador
codifica los tipos de los parámetros dentro del nombre del símbolo, de modo que las dos sobrecargas
salen como cadenas distintas. extern "C" es la instrucción de no decorar este símbolo —a cambio
de renunciar a sobrecargarlo—, y por eso toda biblioteca de C++ pensada para ser llamada desde fuera
publica una fachada extern "C".
Lo importante es lo que sigue: el esquema de decoración nunca se estandarizó. GCC y Clang siguen
el Itanium C++ ABI, MSVC usa uno propio, y ni siquiera dentro de un compilador está garantizada la
estabilidad: el cambio de std::string en GCC 5 (la macro _GLIBCXX_USE_CXX11_ABI) produjo dos
mundos de símbolos incompatibles con el mismo código fuente. Por eso nadie distribuye bibliotecas
C++ para consumo general con su interfaz C++ desnuda, y por eso todos los primos de la clase 156
llamaban a C y no a C++.
Objective-C está en el extremo opuesto por una razón sencilla: es C, un superconjunto estricto.
Una función C dentro de un .m produce el mismo símbolo que produciría en un .c, y no hay puente
que construir. El matiz honesto es que un método de Objective-C no es un símbolo: es un
selector que se resuelve en tiempo de ejecución vía objc_msgSend. Objective-C, sin decirlo, se
parece más a la JVM en cómo llama a sus métodos y más a C en cómo exporta sus funciones.
Representantes del núcleo: Go · Rust. Compilan a nativo, así que todos decoran por defecto y todos tienen una palabra para dejar de hacerlo.
const std = @import("std");
// export publica el símbolo con el ABI de C: nombre sin decorar y argumentos
// donde la plataforma dice. El callconv(.C) es explícito aquí por claridad;
// export ya lo implica. Sin export, Zig no garantiza nombre de símbolo alguno.
export fn abi_compatible(a: c_int, b: c_int) callconv(.C) bool {
return a == b;
}
pub fn main() !void {
var buf: [64]u8 = undefined;
const linea = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')).?;
var it = std.mem.tokenizeAny(u8, linea, " \t\r");
const a = try std.fmt.parseInt(c_int, it.next().?, 10);
const b = try std.fmt.parseInt(c_int, it.next().?, 10);
const veredicto = if (abi_compatible(a, b)) "compatible" else "incompatible";
try std.io.getStdOut().writer().print("abi={s}\n", .{veredicto});
}
import std/strutils
# exportc fija el nombre del símbolo (Nim lo renombraría para evitar colisiones);
# cdecl fija la convención de llamada. Hacen falta las dos cosas, son distintas.
proc abiCompatible(a, b: cint): bool {.exportc: "abi_compatible", cdecl.} =
a == b
let v = stdin.readLine().splitWhitespace()
let a = v[0].parseInt().cint
let b = v[1].parseInt().cint
echo "abi=", (if abiCompatible(a, b): "compatible" else: "incompatible")
import std.stdio, std.array, std.conv, std.algorithm, std.string;
// extern (C) cambia dos cosas a la vez: el mangling (D tiene el suyo propio,
// tan decorado como el de C++) y la convención de llamada.
extern (C) bool abi_compatible(int a, int b) { return a == b; }
void main() {
auto v = readln().strip().split().map!(to!int).array;
writefln("abi=%s", abi_compatible(v[0], v[1]) ? "compatible" : "incompatible");
}
Qué reconocer: los tres tienen la misma estructura de solución que Rust con
#[no_mangle] pub extern "C", y por el mismo motivo: un lenguaje moderno necesita módulos,
genéricos y sobrecarga, y todo eso obliga a decorar los nombres. Salir de la decoración es siempre
una decisión explícita del programador, nunca el valor por defecto.
Fíjate en que Nim necesita dos anotaciones, exportc y cdecl, y que no son lo mismo: la
primera responde cómo se llama el símbolo, la segunda cómo se pasan los argumentos y quién limpia
la pila. Un lenguaje puede acertar una y fallar la otra, y el resultado —símbolo encontrado, pila
corrompida— es el fallo más difícil de diagnosticar de toda esta parte del curso. Nim tiene además
el caso más curioso: como compila a C, su ABI acaba siendo la que le dé el compilador de C que
uses debajo, con todo lo que eso arrastra. Zig cierra por el otro lado: su compilador conoce los
ABI de las plataformas como objetivo de primera clase (-target x86_64-windows-gnu y compañía), y
puede hacer cross-compilation precisamente porque el ABI de destino es un dato explícito y no una
propiedad de la máquina donde compilas.
Representante del núcleo: SQL. Aquí no hay ABI del lenguaje: hay ABI del motor.
:- initialization(main, main).
main :-
read_line_to_string(user_input, Linea),
split_string(Linea, " ", " \t\r", Partes),
maplist([S, N]>>number_string(N, S), Partes, [A, B]),
( A =:= B
-> Veredicto = compatible
; Veredicto = incompatible
),
format("abi=~w~n", [Veredicto]).
% Datalog no tiene E/S: los anchos entran como hechos.
% Fijate en que la compatibilidad no se comprueba con un "=", se comprueba
% usando la MISMA variable W en los dos atomos: es unificacion.
ancho(componente_a, 64).
ancho(componente_b, 64).
abi_compatible :- ancho(componente_a, W), ancho(componente_b, W).
Qué reconocer: en los declarativos la pregunta de la clase se traslada un nivel: el programa no
tiene ABI, pero el motor que lo ejecuta sí, y es con esa con la que hay que ser compatible.
SWI-Prolog carga extensiones nativas que deben exportar install_<nombre>() y hablar con
PL_register_foreign, y ese .so está atado a la versión del intérprete que lo cargará.
El ejemplo más elegante lo da SQL, el representante del núcleo. SQLite resuelve el problema sin
depender del enlazador: una extensión cargable no llama a las funciones de SQLite por su nombre, sino
a través de un puntero a una estructura de punteros a función (sqlite3_api_routines) que el
motor le entrega al cargarla. Como no hay símbolos que resolver, no hay mangling ni convención que
negociar. PostgreSQL eligió el camino contrario y lo hizo explícito: todo módulo en C debe
declarar la macro PG_MODULE_MAGIC, un bloque con la versión y las opciones de compilación que el
servidor comprueba al cargar y rechaza si no coinciden. Son las dos respuestas posibles a esta
clase: o evitas el ABI pasando punteros, o lo declaras y lo verificas. Datalog, por su parte, ni se
plantea la pregunta —no tiene funciones que enlazar—, pero deja de regalo la observación más bonita
de la página: comprobar compatibilidad es unificar dos anchos en la misma variable.
Veinte lenguajes, y una sola pregunta bajo el nombre de cada función: cómo se llama el símbolo, quién pone los argumentos y dónde, y cuánto mide cada uno. Los compilados la responden en el archivo objeto, las máquinas virtuales la evitan con su propio formato de enlace y luego tienen que fabricarla para cruzar, y los intérpretes la heredan del binario que los ejecuta. Nadie se libra: solo cambia quién firma el acuerdo.