Clase 156 — La FFI (Foreign Function Interface): llamar a C desde todos

Parte 10 — Interoperabilidad y fronteras entre lenguajes · ⏱️ Duración estimada: 90 min · Nivel: IntermedioClase construida — 10 implementaciones del núcleo verificadas contra casos.json.


🎯 Objetivo

La frontera más íntima entre dos lenguajes es la llamada directa de función: mi código invoca, dentro del mismo proceso y sin red de por medio, una función compilada por otro lenguaje. El mecanismo que lo hace posible se llama FFI, Foreign Function Interface, y el objetivo de esta clase es entender por qué existe, cómo funciona y por qué —casi sin excepción— el punto de encuentro es C.

La razón es histórica y práctica a la vez. C fue el lenguaje del sistema operativo Unix, y con él nació una convención binaria estable para llamar funciones: cómo se pasan los argumentos, dónde se deja el valor de retorno, quién limpia la pila. Esa convención, la ABI de C (que estudiarás en detalle en la clase 157), es tan simple y tan universal que se convirtió en el mínimo común denominador. Cuando Python necesita velocidad numérica, no reescribe LAPACK: lo llama por FFI. Cuando la JVM necesita tocar el sistema operativo, usa JNI hacia C. C no es el mejor lenguaje para casi nada moderno, pero es el idioma franco en el que todos saben decir "llámame esta función". Entender la FFI es entender por qué C sigue vivo en el centro de ecosistemas que no escriben una sola línea de C.

📚 Resultados de aprendizaje

Al finalizar, podrás:

  1. Explicar qué es la FFI y qué problema resuelve.
  2. Reconocer por qué C es el puente universal entre lenguajes.
  3. Llamar a una función "externa" respetando su firma.
  4. Anticipar los riesgos de cruzar la frontera: tipos, memoria y errores.

🗺️ Temas

# Tema Por qué importa
1 FFI Llamar a código de otro lenguaje en el mismo proceso
2 C como puente Casi todos exponen una FFI hacia C
3 Firma y enlace Declarar tipos exactos y unir con la librería

📖 Definiciones y características

La FFI es la interfaz que permite a un lenguaje llamar funciones definidas en otro, típicamente compiladas a código nativo. Su valor es reutilizar librerías probadas —criptografía, compresión, álgebra— en vez de reescribirlas. Pero reutilizar tiene un precio: debes describir, en tu lenguaje, la firma exacta de la función externa (cuántos argumentos, de qué tipo, qué devuelve). Si te equivocas, no hay un error de compilación amable: hay corrupción de memoria o una caída, porque la frontera es binaria y nadie la revisa por ti.

Una función externa es la que vive en otro lenguaje y se declara para poder invocarla. En Rust se escribe extern "C" { fn doble(x: i64) -> i64; }; en Python se describe con ctypes o cffi; en C# con [DllImport]. La declaración no ejecuta nada: solo le dice al enlazador y al runtime cómo hablar con esa función. Y C como lingua franca es el hecho central: casi todos los lenguajes exponen una FFI hacia C, no hacia cada uno de los demás. Como observa Tanenbaum al hablar de heterogeneidad, un sistema con muchas partes necesita un protocolo común; en la frontera intra-proceso, ese protocolo común es la ABI de C.

🧩 Situación

NumPy no es Python: su corazón es C y Fortran, y Python lo llama por FFI millones de veces por segundo. Ruby carga extensiones nativas en C; la JVM baja a C con JNI para operaciones que el bytecode no cubre; Node.js expone N-API para escribir addons en C++. En todos estos casos ocurre lo mismo: un lenguaje de alto nivel delega el trabajo pesado en una función nativa y recoge el resultado. Para que el concepto se vea sin el ruido de instalar una librería, esta clase simula esa frontera con la función más simple imaginable —duplicar un número— escrita como si viviera en C y llamada desde cada lenguaje. La operación es trivial; lo que importa es el gesto: aquí hay una función que finge venir de C, y todos la saben invocar.

🧮 Modelo

Especificación y verificación en casos.json:

stdin esperado
5 resultado=10
0 resultado=0
7 resultado=14

📐 Algoritmo (pseudocódigo neutral)

declarar doble (externa) ; ESCRIBIR doble(n)

🌐 Implementaciones idiomáticas — el código a la vista

Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json. Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.

Python · python/main.py · python main.py

import sys


def doble(x):  # simula una función externa (FFI hacia C)
    return x * 2


n = int(sys.stdin.readline())
print(f"resultado={doble(n)}")

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

JavaScript · javascript/main.mjs · node main.mjs

import { readFileSync } from "node:fs";

const doble = (x) => x * 2; // función 'externa' vía FFI
const n = parseInt(readFileSync(0, "utf8").trim(), 10);
console.log(`resultado=${doble(n)}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

TypeScript · typescript/main.ts · pnpm exec tsx main.ts

import { readFileSync } from "node:fs";

const doble = (x: number): number => x * 2;
const n: number = parseInt(readFileSync(0, "utf8").trim(), 10);
console.log(`resultado=${doble(n)}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

Java · java/Main.java · java Main.java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static long doble(long x) { return x * 2; } // simula JNI hacia C

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        long n = Long.parseLong(br.readLine().trim());
        System.out.println("resultado=" + doble(n));
    }
}

🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure

C# · csharp/Program.cs · dotnet run

using System;

long Doble(long x) => x * 2; // simula P/Invoke hacia C

long n = long.Parse(Console.In.ReadToEnd().Trim());
Console.WriteLine($"resultado={Doble(n)}");

🧬 El mismo programa en la familia .NET: F# · VB.NET

Go · go/main.go · go run main.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

func doble(x int64) int64 { return x * 2 } // simula cgo hacia C

func main() {
    line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
    n, _ := strconv.ParseInt(strings.TrimSpace(line), 10, 64)
    fmt.Printf("resultado=%d\n", doble(n))
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

Rust · rust/main.rs · rustc main.rs -o main && ./main

use std::io::Read;

fn doble(x: i64) -> i64 {
    x * 2 // en un caso real, una funcion externa con extern "C"
}

fn main() {
    let mut s = String::new();
    std::io::stdin().read_to_string(&mut s).unwrap();
    let n: i64 = s.trim().parse().unwrap();
    println!("resultado={}", doble(n));
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

C · c/main.c · cc main.c -o main && ./main

#include <stdio.h>

long doble(long x) { return x * 2; } /* la funcion nativa en C */

int main(void) {
    long n;
    if (scanf("%ld", &n) != 1) return 1;
    printf("resultado=%ld\n", doble(n));
    return 0;
}

🧬 El mismo programa en la familia C / llaves: C++ · Objective-C

SQL · sql/main.sql · sqlite3 :memory: < main.sql

-- SQL llama a funciones definidas por el usuario; aqui, la expresion.
WITH nums(n) AS (VALUES (5), (0), (7))
SELECT printf('resultado=%d', n * 2) AS resultado FROM nums;

🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog

PHP · php/main.php · php main.php

<?php
function doble($x) { return $x * 2; } // simula una extension en C

$n = (int) trim(fgets(STDIN));
echo "resultado=" . doble($n) . "\n";

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.

🔎 Recorrido del código (laboratorio)

Tomemos el caso 5, que debe producir resultado=10. La entrada es un entero; la salida es su doble. Lo interesante es cómo cada lenguaje representa "la función que vive en C".

En Python, doble(x) se define con un comentario explícito —# simula una función externa (FFI hacia C)— porque en un caso real esa función no sería Python: sería lib.doble obtenida con ctypes.CDLL("libdoble.so"). Aquí se escribe en Python para que el ejemplo corra sin compilar una .so, pero el papel es el de una llamada externa. Se lee n con int(sys.stdin.readline()), se invoca doble(n) y la f-string imprime resultado=10. Fíjate en el detalle semántico: Python convierte el texto "5" a un entero de precisión arbitraria; el "5 de C" sería un long de 64 bits fijos. Esa diferencia de tipos es exactamente lo que la FFI real obliga a declarar.

En C, doble es la función nativa de verdad: long doble(long x) { return x * 2; }. No hay simulación, es el destino que los demás fingen. scanf("%ld", &n) lee el entero y printf("resultado=%ld\n", doble(n)) imprime. Este bloque es la referencia: es la firma que un binding tendría que reproducir sin un solo bit de diferencia (long, un argumento, devuelve long).

En Rust, doble(x: i64) -> i64 lleva el comentario // en un caso real, una funcion externa con extern "C". En producción se escribiría dentro de un bloque extern "C" { fn doble(x: i64) -> i64; } y se llamaría bajo unsafe, porque Rust no puede garantizar la seguridad de memoria al otro lado de la frontera. El i64 de Rust coincide deliberadamente con el long de C de 64 bits: ese emparejamiento de tipos es lo que hace segura la llamada. Los tres lenguajes producen resultado=10, pero solo C ejecuta código nativo; Python y Rust muestran la forma de la frontera.

🔬 Comparación

Lenguaje Mecanismo real de FFI hacia C
Python ctypes / cffi en la stdlib; casi toda librería científica es un binding a C.
JavaScript N-API (addons nativos) o ffi-napi para cargar .so/.dll en tiempo de ejecución.
Java JNI (clásico) y el nuevo Foreign Function & Memory API (Project Panama, java.lang.foreign).
C# P/Invoke con [DllImport]; el runtime hace el marshalling de tipos.
Go cgo: import "C" con un preámbulo de C incrustado en comentarios.
Rust extern "C" + unsafe; la crate libc y bindgen generan las firmas.
C Es el destino: no necesita FFI, expone la firma.
PHP Extensión Zend en C, o la clase FFI desde PHP 7.4.

La columna revela el punto de la clase: la sintaxis cambia radicalmente —ctypes no se parece en nada a extern "C"— pero todas apuntan a lo mismo, C, y todas resuelven el mismo problema semántico: emparejar tipos a través de una frontera binaria. La diferencia peligrosa no es sintáctica sino semántica: si declaras int (32 bits) donde C espera long (64 bits en Linux), la llamada compila y corrompe datos en silencio. Por eso Java está migrando de JNI —verboso y frágil— al API de Panama, más seguro y tipado. SQL queda aparte: sus "funciones externas" son UDF (funciones definidas por el usuario) registradas en el motor, un modelo distinto al de la FFI intra-proceso.

🧬 El concepto en la familia

El patrón "todos hacia C" se repite en cada familia: ctypes en Python, N-API en Node, JNI y Panama en la JVM, P/Invoke en .NET, cgo en Go, extern "C" en Rust y C++. Incluso lenguajes muy alejados —Haskell con su FFI, Lua con su C API— convergen en la misma ABI. C es el esperanto de las bibliotecas nativas: nadie lo elige por gusto, todos lo hablan por necesidad.

✅ Prueba común

Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:

python scripts/verificar_equivalencia.py 156

🧪 Reto de transferencia

Detalle en reto.md.

⚠️ Errores comunes

❓ Preguntas frecuentes

🔗 Referencias

Libros de la parte:

Libros de los lenguajes del núcleo:


⏮️ Clase 155 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 157 ⏭️