INGENIERÍA

Cómo compila Ferric Rust en un iPad

Sin nube, sin servidor de compilación remoto, sin JIT. El toolchain de Rust entero corre como WebAssembly dentro de una vista web, y todo lo demás existe para que eso sea lo bastante rápido como para sentirse un IDE.

Las reglas del juego

iPadOS no permite que una app ejecute código descargado, genere código nativo en tiempo de ejecución ni cargue con dlopen una librería que no venía en el paquete. Un compilador es, por definición, un programa que produce código. El único motor en el que una app puede ejecutar código arbitrario es el que usa Safari: JavaScriptCore, que también ejecuta WebAssembly. Así que el compilador tiene que ser WebAssembly, lo que produce tiene que ser WebAssembly o una imagen de firmware para otro chip, y el toolchain tiene que viajar dentro de la app.

Ferric sigue esas reglas al pie de la letra. rustc, cargo, lld y rust-analyzer están compilados a wasm32-wasip1 y corren dentro de un WKWebView. Nada nativo se carga en tiempo de ejecución. No hay telemetría ni cuenta. El toolchain pesa 621 MB en 4.158 archivos, empaquetado en un solo Apple Archive de 198 MB, porque App Review rechaza archivos .a y .o sueltos dentro del paquete. Se descomprime una vez, en el primer arranque.

Un toolchain que cree que tiene sistema operativo

rustc espera archivos, un reloj, variables de entorno, procesos e hilos. WASI le da los tres primeros. El resto se emula en JavaScript, y en esos shims viven las sorpresas. thread::sleep funciona a través de poll_oneoff. Leer de stdin no puede bloquear un web worker, y SharedArrayBuffer no está disponible para una app que sirve sus páginas desde un esquema de URL propio, así que un programa que lee entrada hace una XMLHttpRequest síncrona al host, que responde cuando el usuario escribe. Los sockets devuelven ENOSYS: un programa que compilas no puede abrir conexiones.

La memoria es la restricción real. Una instancia wasm32 direcciona como máximo 4 GB, y un crate grande puede llevar a rustc más allá de lo que un solo proceso de contenido web recibe antes de que iPadOS lo termine. Ferric compila cada unidad en un proceso aislado, ejecuta varios en paralelo a través de un orquestador, y recicla procesos según un plan en vez de esperar a que mueran. Cuando una unidad muere de la misma manera tres builds seguidos, la app lo dice, en vez de reintentar para siempre.

Forge: cargo, portado a C#

cargo compilado a WebAssembly funciona, pero arranca despacio y quiere hilos que el runtime no le puede dar. Ferric sustituyó la mitad planificadora de cargo por un port nativo, Forge: el resolutor de dependencias, la unificación de features, el escritor del lockfile y el plan de compilación. Produce un Cargo.lock idéntico byte a byte al de cargo, que es lo que mantiene las plantillas y los proyectos del usuario portables a un escritorio.

Forge también es dueño de la caché. Cada unidad de compilación tiene una huella derivada solo del contenido, nunca de rutas, así que una dependencia compilada vale para cualquier proyecto que resuelva el mismo crate con las mismas features en el mismo toolchain. Esa caché es global al dispositivo y se puede sembrar: la plantilla embassy para la Pico 2 resuelve a 94 crates y 187 unidades, que tardan 158 segundos en compilarse desde cero en un iPad Air de 11 pulgadas (M3). Embarcar una semilla de 76 MB con esas unidades, generada en un iPad con el toolchain exacto del paquete, deja la primera compilación en unos diez segundos.

Proc-macros como reactores

Una macro procedural es un plugin del compilador: rustc la carga como librería nativa y la llama mientras parsea. Aquí eso es imposible por partida doble. Ferric sigue el enfoque que inauguró watt, de dtolnay: cada crate de proc-macro se compila una vez a un reactor de WebAssembly autocontenido, con proc-macro2 en modo fallback y sin bridge de vuelta al compilador. Un parche pequeño en rustc serializa el flujo de tokens como texto, invoca el reactor una sola vez por un hook síncrono y vuelve a parsear la salida. Sin callbacks no hace falta asyncify ni un segundo proceso. El precio es la higiene: los spans vuelven como spans de sitio de llamada, y unas pocas macros que dependen de spans reales necesitaron parches puntuales para que sus errores dejaran de aparecer solo en el dispositivo.

rust-analyzer en el editor

El editor tiene dos servidores de lenguaje. Uno basado en tree-sitter se encarga de resaltado, plegado, símbolos y navegación rápida, y es tan barato que corre siempre. El segundo es rust-analyzer en sí, compilado a wasm32-wasip1 a través de un shim pequeño, 13 MB, corriendo en un web worker. Recibe el grafo de crates del plan de Forge, con features, cfg y edición por crate, y desempaqueta las fuentes de las dependencias dentro del worker para que 15 MB de código nunca crucen el hilo del editor. Calienta en unos tres segundos y llega a 220 MB en un programa pequeño y 522 MB en la plantilla embassy. Da tipos reales al pasar por encima, definiciones dentro de las dependencias, hints de parámetros y de tipos, y completado. Las macros procedurales aún no se expanden, así que en proyectos Cargo rust-analyzer responde consultas pero nunca publica diagnósticos, para no dar errores falsos.

Ferric Link: una consola serie que no te roba el WiFi

Un iPad no tiene puerto serie ni API para hablar con un dispositivo USB. Sí tiene Ethernet por USB. El firmware que Ferric compila para la Pico incluye una pila USB pequeña (embassy-usb) que presenta la placa como un adaptador de red CDC-NCM, más un servidor DHCP que le da al iPad una dirección en 10.55.0.0/24. La app abre una conexión TCP a la placa y transmite logs en ambos sentidos. Eso es todo el Link: sin drivers, sin permisos especiales, sin más adaptador que un cable USB.

Dos detalles lo hicieron funcionar. El lease de DHCP no lleva a propósito ni gateway ni DNS, así que el iPad conserva el WiFi como ruta por defecto y la red sigue funcionando con la placa conectada. Y Network.framework no reporta interfaces sin ruta por defecto, así que la app vigila la placa con getifaddrs y ata su conexión a la interfaz de forma explícita.

También había un bug en iPadOS. Cuando un dispositivo NCM declara que no tiene capacidades de red, el host selecciona la interfaz de datos y la deselecciona de inmediato, para siempre, y la placa nunca recibe dirección. Anunciar el bit de capacidades y aceptar la petición SetEthernetPacketFilter lo arregla; ese mismo cambio ya existe como pull request abierto de los contribuidores de embassy-usb, y Ferric lo embarca como parche hasta que entre.

Un emulador para comprobar el firmware antes de que salga del iPad

Después de cada compilación embebida, Ferric arranca el firmware en un emulador de RP2040 o RP2350 escrito en C# y comprueba que arranca, que los pines que la lección espera se configuraron, y que el LED de verdad parpadea. No es un simulador interactivo; es un preflight. También es lo que permite que las lecciones embebidas funcionen sin placa.

Lo que aún no hace

rust-analyzer no expande macros procedurales, así que los ítems generados por derives o por crates como paste le resultan invisibles. El depurador de nivel fuente funciona con los programas que corren en el propio iPad. Las únicas placas soportadas son la familia Raspberry Pi Pico. Los programas compilados para el propio iPad corren como WebAssembly sin acceso a red. Cada una de esas cosas es una cantidad de trabajo conocida, y ninguna necesita que cambie una regla.

Prueba la beta

Ferric está en beta pública en TestFlight. Necesita un iPad. La Pico 2 es opcional; el emulador cubre las lecciones.