web-dev-qa-db-fra.com

Comment utiliser une bibliothèque C dans une bibliothèque Rust compilée dans WebAssembly?

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.

26
olanod

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 clangwasm: 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.

Succès minimal: somme de trois chiffres

À 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.

Histoire parallèle: chaîne

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 fichiers rlib, contrairement aux fichiers staticlib, sont interprétés par le compilateur Rust dans les futures liaisons. Cela signifie essentiellement que rustc recherchera les métadonnées dans les fichiers rlib 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 sorties staticlib.

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)

Nouvelle semaine, nouvelles aventures, avec Binaryen, 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:

  • si la fusion finale est exécutée avec le commutateur -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)
  • l'ordre de fusion est important, le fichier avec "Rust-Origin" doit venir en premier. 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

  • probablement ma faute, mais j'ai dû construire un module 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)
  • liés au précédent: de cette façon (construction d'un module complet) les données du module secondaire ne peuvent pas être déplacées par 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).

14
tevemadar