wasm tanks started mdn getting javascript c rust webassembly

javascript - tanks - web assembly wiki



¿Cómo uso una biblioteca C en una biblioteca Rust compilada para WebAssembly? (1)

Estoy experimentando con la interoperabilidad de Rust, WebAssembly y C para usar finalmente la biblioteca Rust (con dependencia de C estática) en el navegador o Node.js. Estoy usando wasm-bindgen para el código de pegamento de JavaScript.

#![feature(libc, use_extern_macros)] extern crate wasm_bindgen; use wasm_bindgen::prelude::*; use std::os::raw::c_char; use std::ffi::CStr; extern "C" { fn hello() -> *const c_char; // returns "hello from C" } #[wasm_bindgen] pub fn greet() -> String { let c_msg = unsafe { CStr::from_ptr(hello()) }; format!("{} and Rust!", c_msg.to_str().unwrap()) }

Mi primer enfoque ingenuo fue tener un script build.rs que use la caja gcc para generar una biblioteca estática a partir del código C. Antes de introducir los bits WASM, pude compilar el programa Rust y ver el hello from C salida de hello from C en la consola, ahora recibo un error del compilador que dice:

rust-lld: error: unknown file type: hello.o

construir.rs

extern crate gcc; fn main() { gcc::Build::new() .file("src/hello.c") .compile("libhello.a"); }

Esto tiene sentido, ahora que lo pienso, ya que el archivo hello.o se compiló para la arquitectura de mi computadora portátil y no para el hello.o .

Idealmente, me gustaría que esto funcionara de inmediato, agregando un poco de magia en mis build.rs que, por ejemplo, compilaría la biblioteca de C para que fuera una biblioteca de ensamblajes web estática que Rust puede usar.

Lo que creo que podría funcionar, pero me gustaría evitarlo, ya que suena más problemático, es usar Emscripten para crear una biblioteca WASM para el código C, luego compilar la biblioteca Rust por separado y pegarlas en JavaScript.


TL; DR: Salta a " Nueva semana, nuevas aventuras " para obtener "¡Hola de C y Rust!"

La mejor manera sería crear una biblioteca WASM y pasarla al enlazador. rustc tiene una opción para eso (y parece que también hay directivas de código fuente):

rustc <yourcode.rs> --target wasm32-unknown-unknown --crate-type=cdylib -C link-arg=<library.wasm>

El truco es que la biblioteca tiene que ser una biblioteca, por lo que debe contener las reloc (y en la práctica). Emscripten parece tener un símbolo para eso, RELOCATABLE :

emcc <something.c> -s WASM=1 -s SIDE_MODULE=1 -s RELOCATABLE=1 -s EMULATED_FUNCTION_POINTERS=1 -s ONLY_MY_CODE=1 -o <something.wasm>

( EMULATED_FUNCTION_POINTERS se incluye con RELOCATABLE , por lo que no es realmente necesario, ONLY_MY_CODE algunos extras, pero tampoco importa aquí)

La emcc es que emcc nunca generó un archivo wasm reubicable para mí, al menos no la versión que descargué esta semana, para Windows (jugué esto en dificultad difícil, lo que retrospectivamente podría no haber sido la mejor idea). Por lo tanto, faltan las secciones y rustc sigue quejándose de que <something.wasm> is not a relocatable wasm file .

Luego viene clang , que puede generar un módulo wasm reubicable con un sencillo de una sola línea:

clang -c <something.c> -o <something.wasm> --target=wasm32-unknown-unknown

Entonces rustc dice "La subsección de enlaces terminó prematuramente". Ah, sí (por cierto, mi configuración de Rust también era nueva). Luego leí que hay dos objetivos de wasm32-unknown-unknown-wasm wasm : wasm32-unknown-unknown-wasm y wasm32-unknown-unknown-elf , y tal vez este último debería usarse aquí. Como mi nueva llvm+clang compilación llvm+clang encuentra con un error interno con este objetivo, y me pide que envíe un informe de error a los desarrolladores, podría ser algo para probar en fácil o mediano, como en algunos * nix o Mac box.

Minimal éxito: suma de tres números

En este punto, acabo de agregar lld a llvm y llvm vincular manualmente un código de prueba desde archivos de código de bits:

clang cadd.c --target=wasm32-unknown-unknown -emit-llvm -c rustc rsum.rs --target wasm32-unknown-unknown --crate-type=cdylib --emit llvm-bc lld -flavor wasm rsum.bc cadd.bc -o msum.wasm --no-entry

Aw sí, suma números, 2 en C y 1 + 2 en Rust:

cadd.c

int cadd(int x,int y){ return x+y; }

msum.rs

extern "C" { fn cadd(x: i32, y: i32) -> i32; } #[no_mangle] pub fn rsum(x: i32, y: i32, z: i32) -> i32 { x + unsafe { cadd(y, z) } }

test.html

<script> fetch(''msum.wasm'') .then(response => response.arrayBuffer()) .then(bytes => WebAssembly.compile(bytes)) .then(module => { console.log(WebAssembly.Module.exports(module)); console.log(WebAssembly.Module.imports(module)); return WebAssembly.instantiate(module, { env:{ _ZN4core9panicking5panic17hfbb77505dc622acdE:alert } }); }) .then(instance => { alert(instance.exports.rsum(13,14,15)); }); </script>

Que _ZN4core9panicking5panic17hfbb77505dc622acdE siente muy natural (el módulo se compila y _ZN4core9panicking5panic17hfbb77505dc622acdE una instancia en dos pasos para registrar las exportaciones e importaciones, que es una forma de encontrar esas piezas faltantes), y pronostica la desaparición de este intento: todo esto funciona porque no hay otra referencia a la biblioteca de tiempo de ejecución, y este método en particular podría ser simulado / proporcionado manualmente.

Historia lateral: cadena

Dado que alloc y su Layout asustaron un poco, alloc por el enfoque basado en vectores descrito / usado de vez en cuando, por ejemplo here o en Hello, Rust! .
Aquí hay un ejemplo, obteniendo la cadena "Hola de ..." del exterior ...

rhello.rs

use std::ffi::CStr; use std::mem; use std::os::raw::{c_char, c_void}; use std::ptr; extern "C" { fn chello() -> *mut c_char; } #[no_mangle] pub fn alloc(size: usize) -> *mut c_void { let mut buf = Vec::with_capacity(size); let p = buf.as_mut_ptr(); mem::forget(buf); p as *mut c_void } #[no_mangle] pub fn dealloc(p: *mut c_void, size: usize) { unsafe { let _ = Vec::from_raw_parts(p, 0, size); } } #[no_mangle] pub fn hello() -> *mut c_char { let phello = unsafe { chello() }; let c_msg = unsafe { CStr::from_ptr(phello) }; let message = format!("{} and Rust!", c_msg.to_str().unwrap()); dealloc(phello as *mut c_void, c_msg.to_bytes().len() + 1); let bytes = message.as_bytes(); let len = message.len(); let p = alloc(len + 1) as *mut u8; unsafe { for i in 0..len as isize { ptr::write(p.offset(i), bytes[i as usize]); } ptr::write(p.offset(len as isize), 0); } p as *mut c_char }

Construido como rustc rhello.rs --target wasm32-unknown-unknown --crate-type=cdylib

... y trabajando realmente con JavaScript :

jhello.html

<script> var e; fetch(''rhello.wasm'') .then(response => response.arrayBuffer()) .then(bytes => WebAssembly.compile(bytes)) .then(module => { console.log(WebAssembly.Module.exports(module)); console.log(WebAssembly.Module.imports(module)); return WebAssembly.instantiate(module, { env:{ chello:function(){ var s="Hello from JavaScript"; var p=e.alloc(s.length+1); var m=new Uint8Array(e.memory.buffer); for(var i=0;i<s.length;i++) m[p+i]=s.charCodeAt(i); m[s.length]=0; return p; } } }); }) .then(instance => { /*var*/ e=instance.exports; var ptr=e.hello(); var optr=ptr; var m=new Uint8Array(e.memory.buffer); var s=""; while(m[ptr]!=0) s+=String.fromCharCode(m[ptr++]); e.dealloc(optr,s.length+1); console.log(s); }); </script>

No es particularmente hermoso (en realidad no tengo idea de Rust), pero hace algo de lo que espero de él, e incluso ese dealloc podría funcionar (al menos invocarlo dos veces produce un pánico).
Había una importante lección en camino: cuando el módulo administra su memoria, su tamaño puede cambiar, lo que da como resultado la invalidación del objeto ArrayBuffer respaldo y sus vistas. Es por eso que memory.buffer se comprueba varias veces y se verifica después de llamar al código wasm .

Y aquí es donde estoy atascado, porque este código se referiría a bibliotecas de tiempo de ejecución, y .rlib -s. Lo más cercano a una compilación manual es lo siguiente:

rustc rhello.rs --target wasm32-unknown-unknown --crate-type=cdylib --emit obj lld -flavor wasm rhello.o -o rhello.wasm --no-entry --allow-undefined liballoc-5235bf36189564a3.rlib liballoc_system-f0b9538845741d3e.rlib libcompiler_builtins-874d313336916306.rlib libcore-5725e7f9b84bd931.rlib libdlmalloc-fffd4efad67b62a4.rlib liblibc-453d825a151d7dec.rlib libpanic_abort-43290913ef2070ae.rlib libstd-dcc98be97614a8b6.rlib libunwind-8cd3b0417a81fb26.rlib

Donde tuve que usar el lld sentado en las profundidades de la cadena de herramientas de Rust como se dice .rlib -s se interpreted , por lo que están vinculados a la cadena de herramientas de Rust

--crate-type=rlib , #[crate_type = "rlib"] - Se producirá un archivo "Rust library". Esto se utiliza como un artefacto intermedio y puede considerarse como una "biblioteca estática de Rust". Estos archivos rlib , a diferencia de los archivos staticlib , son interpretados por el compilador Rust en un enlace futuro. Básicamente, esto significa que rustc buscará metadatos en archivos rlib como busca metadatos en bibliotecas dinámicas. Esta forma de salida se utiliza para producir ejecutables enlazados estáticamente, así como salidas staticlib .

Por supuesto, este lld no come los archivos .wasm / .o generados con clang o llc ("La subsección de enlaces terminó prematuramente"), tal vez la parte Rust también debería reconstruirse con el llvm personalizado.
Además, a esta compilación parece faltarle los asignadores reales, además de chello , habrá 4 entradas más en la tabla de importación: __rust_alloc , __rust_alloc_zeroed , __rust_dealloc y __rust_realloc . Lo que de hecho podría proporcionarse desde JavaScript después de todo, simplemente anula la idea de dejar que Rust maneje su propia memoria, además de que un asignador estaba presente en la compilación de rustc solo paso ... Oh, sí, aquí es donde me di por vencido. semana (11 de agosto de 2018, a las 21:56)

Nueva semana, nuevas aventuras, con Binaryen, wasm-dis/merge

La idea era modificar el código Rust ya hecho (tener asignadores y todo en su lugar). Y este funciona. Mientras su código C no tenga datos.

Código de prueba de concepto:

chello.c

void *alloc(int len); // allocator comes from Rust char *chello(){ char *hell=alloc(13); hell[0]=''H''; hell[1]=''e''; hell[2]=''l''; hell[3]=''l''; hell[4]=''o''; hell[5]='' ''; hell[6]=''f''; hell[7]=''r''; hell[8]=''o''; hell[9]=''m''; hell[10]='' ''; hell[11]=''C''; hell[12]=0; return hell; }

No es extremadamente habitual, pero es el código C.

rustc rhello.rs --target wasm32-unknown-unknown --crate-type=cdylib wasm-dis rhello.wasm -o rhello.wast clang chello.c --target=wasm32-unknown-unknown -nostdlib -Wl,--no-entry,--export=chello,--allow-undefined wasm-dis a.out -o chello.wast wasm-merge rhello.wast chello.wast -o mhello.wasm -O

( rhello.rs es el mismo que se presenta en "Side story: string")
Y el resultado funciona como

mhello.html

<script> fetch(''mhello.wasm'') .then(response => response.arrayBuffer()) .then(bytes => WebAssembly.compile(bytes)) .then(module => { console.log(WebAssembly.Module.exports(module)); console.log(WebAssembly.Module.imports(module)); return WebAssembly.instantiate(module, { env:{ memoryBase: 0, tableBase: 0 } }); }) .then(instance => { var e=instance.exports; var ptr=e.hello(); console.log(ptr); var optr=ptr; var m=new Uint8Array(e.memory.buffer); var s=""; while(m[ptr]!=0) s+=String.fromCharCode(m[ptr++]); e.dealloc(optr,s.length+1); console.log(s); }); </script>

Incluso los asignadores parecen hacer algo ( ptr lecturas de bloques repetidos con / sin dealloc muestran cómo la memoria no tiene fugas / fugas en consecuencia).

Por supuesto, esto es súper frágil y tiene partes misteriosas también:

  • si la combinación final se ejecuta con el -S (genera código fuente en lugar de .wasm ), y el archivo de ensamblaje del resultado se compila por separado (usando wasm-as ), el resultado será un par de bytes más corto (y esos bytes están en algún lugar en la mitad del código en ejecución, no en las secciones de exportación / importación / datos)
  • El orden de fusión es importante, el archivo con "origen de óxido" debe aparecer primero. wasm-merge chello.wast rhello.wast [...] muere wasm-merge chello.wast rhello.wast [...] con un mensaje entretenido

    [error del validador de wasm en el módulo] inesperado falso: el desplazamiento del segmento debe ser razonable, en
    [i32] (i32.const 1)
    Fatal: error en la validación de salida

  • Probablemente sea mi culpa, pero tuve que construir un módulo chello.wasm completo (así, con vinculación). Compilar solo ( clang -c [...] ) dio como resultado el módulo reubicable que se perdió tanto al principio de esta historia, pero al descompilarlo (a .wast ) perdimos la exportación nombrada ( chello() ):
    (export "chello" (func $chello)) desaparece completamente
    (func $chello ... convierte en (func $0 ... , una función interna ( wasm-dis pierde reloc y linking secciones, poniendo solo un comentario sobre ellas y su tamaño en la fuente del ensamblaje)
  • relacionado con el anterior: de esta manera (construyendo un módulo completo) los datos del módulo secundario no pueden ser reubicados por wasm-merge : mientras existe la posibilidad de capturar referencias a la cadena ( const char *HELLO="Hello from C"; convierte en una constante en el desplazamiento 1024 en particular, y luego se denomina (i32.const 1024) si es una constante local, dentro de una función), no sucede. Y si es una constante global, su dirección también se convierte en una constante global, el número 1024 almacenado en el desplazamiento 1040, y la cadena se denominará (i32.load offset=1040 [...] , lo que comienza a ser difícil captura.

Para las risas, este código compila y funciona también ...

void *alloc(int len); int my_strlen(const char *ptr){ int ret=0; while(*ptr++)ret++; return ret; } char *my_strcpy(char *dst,const char *src){ char *ret=dst; while(*src)*dst++=*src++; *dst=0; return ret; } char *chello(){ const char *HELLO="Hello from C"; char *hell=alloc(my_strlen(HELLO)+1); return my_strcpy(hell,HELLO); }

... solo escribe "Hello from C" en medio del grupo de mensajes de Rust, lo que da como resultado la impresión

Hola de Clt :: unwrap () `en un valor` Err`an y Rust!

(Explicación: 0-inicializadores no están presentes en el código recompilado debido a la marca de optimización, -O )
Y también hace surgir la pregunta sobre la ubicación de un libc (aunque los define sin my_ , clang menciona strlen y strcpy como incorporados, también les dice que están correctamente escritos, no emite código para ellos y se convierten en importaciones para el módulo resultante) .