J'expérimente avec Rust, WebAssembly et l'interopérabilité C pour éventuellement utiliser la bibliothèque Rust (avec dépendance C statique) dans le navigateur ou Node.js. J'utilise wasm-bindgen
pour le code de collage 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())
}
Ma première approche naïve a été d'avoir un build.rs
script qui utilise la caisse gcc pour générer une bibliothèque statique à partir du code C. Avant d'introduire les bits WASM, j'ai pu compiler le programme Rust et voir le hello from C
sortie dans la console, maintenant je reçois une erreur du compilateur disant
Rust-lld: error: unknown file type: hello.o
build.rs
extern crate gcc;
fn main() {
gcc::Build::new()
.file("src/hello.c")
.compile("libhello.a");
}
Cela a du sens, maintenant que j'y pense, puisque le hello.o
le fichier a été compilé pour l'architecture de mon ordinateur portable et non pour WebAssembly.
Idéalement, j'aimerais que cela fonctionne dès le départ en ajoutant de la magie dans mon build.rs qui compilerait par exemple la bibliothèque C pour être une bibliothèque WebAssembly statique que Rust peut utiliser.
Ce que je pense que cela pourrait fonctionner, mais j'aimerais éviter car cela semble plus problématique, est d'utiliser Emscripten pour créer une bibliothèque WASM pour le code C, puis compiler la bibliothèque Rust séparément et les coller ensemble dans JavaScript.
TL; DR: Aller à " Nouvelle semaine, nouvelles aventures " pour obtenir "Bonjour de C et Rust!"
La bonne façon serait de créer une bibliothèque WASM et de la transmettre à l'éditeur de liens. rustc
a une option pour cela (et il semble y avoir aussi des directives de code source):
rustc <yourcode.rs> --target wasm32-unknown-unknown --crate-type=cdylib -C link-arg=<library.wasm>
L'astuce est que la bibliothèque doit être une bibliothèque, elle doit donc contenir des sections reloc
(et en pratique linking
). Emscripten semble avoir un symbole pour cela, 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
Est inclus avec RELOCATABLE
, donc ce n'est pas vraiment nécessaire, ONLY_MY_CODE
Supprime certains extras, mais cela n'a pas d'importance ici non plus)
Le fait est que emcc
n'a jamais généré de fichier wasm
déplaçable pour moi, du moins pas la version que j'ai téléchargée cette semaine, pour Windows (j'ai joué cela en difficulté difficile, qui rétrospectivement pourrait ne pas avoir été la meilleure idée). Les sections sont donc manquantes et rustc
continue de se plaindre de <something.wasm> is not a relocatable wasm file
.
Vient ensuite clang
, qui peut générer un module wasm
relocalisable avec un simple liner:
clang -c <something.c> -o <something.wasm> --target=wasm32-unknown-unknown
rustc
dit alors "La sous-section de liaison s'est terminée prématurément". Oh, oui (au fait, ma Rust était toute neuve également). Ensuite, j'ai lu qu'il y avait deux cibles clang
wasm
: wasm32-unknown-unknown-wasm
Et wasm32-unknown-unknown-elf
, Et peut-être que ce dernier devrait être utilisé ici. Comme mon tout nouveau build llvm+clang
Rencontre une erreur interne avec cette cible, me demandant d'envoyer un rapport d'erreur à les développeurs, il pourrait être quelque chose à tester sur facile ou moyen, comme sur certains * nix ou box Mac.
À ce stade, je viens d'ajouter lld
à llvm
et j'ai réussi à lier manuellement un code de test à partir de fichiers bitcode:
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 oui, il résume les nombres, 2 en C
et 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
Semble très naturel (le module est compilé et instancié en deux étapes afin de consigner les exportations et les importations, c'est une façon de trouver de telles pièces manquantes), et prévoit la fin de cette tentative: l'ensemble fonctionne car il n'y a aucune autre référence à la bibliothèque d'exécution, et cette méthode particulière pourrait être simulée/fournie manuellement.
Comme alloc
et son Layout
chose me faisait un peu peur, je suis allé avec l'approche vectorielle décrite/utilisée de temps en temps, par exemple ici ou sur Bonjour, Rust! .
Voici un exemple, obtenir la chaîne "Hello from ..." de l'extérieur ...
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
}
Conçu comme rustc rhello.rs --target wasm32-unknown-unknown --crate-type=cdylib
... et réellement travailler avec 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>
Ce n'est pas particulièrement beau (en fait, je n'ai aucune idée de Rust), mais il fait quelque chose que j'attends de lui, et même que dealloc
pourrait fonctionner (au moins l'invoquer deux fois jette une panique).
Il y avait une leçon importante sur le chemin: lorsque le module gère sa mémoire, sa taille peut changer, ce qui entraîne l'invalidation de l'objet de support ArrayBuffer
et de ses vues. C'est pourquoi memory.buffer
Est vérifié plusieurs fois et vérifié après appelant dans le code wasm
.
Et c'est là que je suis bloqué, car ce code ferait référence aux bibliothèques d'exécution et à .rlib
- s. Le plus proche que j'ai pu obtenir d'une construction manuelle est le suivant:
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
Où j'ai dû utiliser le lld
assis dans les profondeurs de la chaîne d'outils Rust comme .rlib
- les s seraient interprétés , ils sont donc liés à la chaîne d'outils Rust
--crate-type=rlib
,#[crate_type = "rlib"]
- Un fichier "bibliothèque Rust" sera produit. Ceci est utilisé comme un artefact intermédiaire et peut être considéré comme une "bibliothèque statique Rust bibliothèque". Ces fichiersrlib
, contrairement aux fichiersstaticlib
, sont interprétés par le compilateur Rust dans les futures liaisons. Cela signifie essentiellement querustc
recherchera les métadonnées dans les fichiersrlib
comme il recherche les métadonnées dans les bibliothèques dynamiques. Cette forme de La sortie est utilisée pour produire des exécutables liés statiquement ainsi que des sortiesstaticlib
.
Bien sûr, ce lld
ne mange pas les fichiers .wasm
/.o
Générés avec clang
ou llc
("La sous-section de liaison s'est terminée prématurément "), peut-être que la partie Rust devrait également être reconstruite avec le llvm
personnalisé.
De plus, cette version semble manquer les allocateurs réels, outre chello
, il y aura 4 entrées supplémentaires dans la table d'importation: __Rust_alloc
, __Rust_alloc_zeroed
, __Rust_dealloc
Et __Rust_realloc
. Ce qui pourrait en fait être fourni par JavaScript après tout, va juste à l'encontre de l'idée de laisser Rust gérer sa propre mémoire, plus un allocateur était présent dans la construction rustc
en un seul passage). .. Oh, oui, c'est là que j'ai abandonné cette semaine (11 août 2018, à 21:56)
wasm-dis/merge
L'idée était de modifier le code prêt à l'emploi Rust (ayant des allocateurs et tout en place). Et celui-ci fonctionne. Tant que votre code C n'a pas de données.
Code de preuve de concept:
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;
}
Pas très habituel, mais c'est du code 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
Est le même que celui présenté dans "Side story: string")
Et le résultat fonctionne comme
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>
Même les allocateurs semblent faire quelque chose (ptr
les lectures de blocs répétés avec/sans dealloc
montrent comment la mémoire ne fuit pas/fuit en conséquence).
Bien sûr, c'est super-fragile et a aussi des parties mystérieuses:
-S
(génère le code source au lieu de .wasm
) et que le fichier d'assemblage de résultat est compilé séparément (à l'aide de wasm-as
), le résultat sera un couple d'octets plus court (et ces octets sont quelque part au milieu du code en cours d'exécution, pas dans les sections d'exportation/importation/données)wasm-merge chello.wast rhello.wast [...]
Meurt avec un message divertissant [erreur de validateur de wasm dans le module] faux inattendu: le décalage de segment doit être raisonnable, activé
[i32] (i32.const 1)
Fatal: erreur lors de la validation de la sortie
chello.wasm
complet (donc, avec un lien). La compilation uniquement (clang -c [...]
) A entraîné le module déplaçable qui manquait tellement au tout début de cette histoire, mais la décompilation de celui-ci (vers .wast
) A perdu l'exportation nommée (chello()
):(export "chello" (func $chello))
Disparaît complètement(func $chello ...
Devient (func $0 ...
, Une fonction interne (wasm-dis
Perd les sections reloc
et linking
, ne faisant qu'une remarque sur eux et leur taille dans la source de l'Assemblée)wasm-merge
: alors qu'il y a une chance d'attraper des références à la chaîne elle-même (const char *HELLO="Hello from C";
devient une constante à l'offset 1024 en particulier, et plus tard appelée (i32.const 1024)
si c'est une constante locale, à l'intérieur d'une fonction), cela ne se produit pas. Et s'il s'agit d'une constante globale, son adresse devient également une constante globale, le numéro 1024 étant stocké à l'offset 1040, et la chaîne va être appelée (i32.load offset=1040 [...]
, Ce qui commence à être difficile à attraper.Pour rire, ce code compile et fonctionne aussi ...
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);
}
... juste il écrit "Bonjour de C" au milieu du groupe de messages de Rust, ce qui entraîne l'impression
Bonjour de Clt :: unwrap () `sur une valeur` Err`an et Rust!
(Explication: les initialiseurs 0 ne sont pas présents dans le code recompilé en raison de l'indicateur d'optimisation, -O
)
Et cela soulève également la question de la localisation d'un libc
(bien que les définissant sans my_
, clang
mentionne strlen
et strcpy
en tant que intégrés, indiquant également leurs singatures correctes, il n'émet pas de code pour eux et ils deviennent des importations pour le module résultant).