Wie verwende ich eine C-Bibliothek in einer zu WebAssembly kompilierten Rust-Bibliothek?

Lesezeit: 13 Minuten

Benutzeravatar von olanod
Olanod

Ich experimentiere mit Rust, WebAssembly und C-Interoperabilität, um schließlich die Rust-Bibliothek (mit statischer C-Abhängigkeit) im Browser oder in Node.js zu verwenden. Ich benutze wasm-bindgen für den JavaScript-Glue-Code.

#![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())
}

Mein erster naiver Ansatz war, a zu haben build.rs Skript, das die gcc-Kiste verwendet, um eine statische Bibliothek aus dem C-Code zu generieren. Bevor ich die WASM-Bits einführte, konnte ich das Rust-Programm kompilieren und die hello from C Ausgabe in der Konsole, jetzt bekomme ich eine Fehlermeldung vom Compiler

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");
}

Das macht Sinn, jetzt wo ich darüber nachdenke, da die hello.o Datei wurde für die Architektur meines Laptops kompiliert, nicht für WebAssembly.

Idealerweise möchte ich, dass dies sofort einsatzbereit ist, indem ich meiner build.rs etwas Magie hinzufüge, die zum Beispiel die C-Bibliothek zu einer statischen WebAssembly-Bibliothek kompilieren würde, die Rust verwenden kann.

Was meiner Meinung nach funktionieren könnte, aber vermeiden möchte, da es problematischer klingt, ist die Verwendung von Emscripten, um eine WASM-Bibliothek für den C-Code zu erstellen, dann die Rust-Bibliothek separat zu kompilieren und sie in JavaScript zusammenzufügen.

  • Die Verwendung von Emscripten ist das einzige Art, dies zu tun, da Emscripten ein Tool ist, um beliebigen C-Code (eigentlich alles, was LLVM unterstützen kann) für WebAssembly zu kompilieren. Ich bin jedoch viel zu faul, um herauszufinden, wie ich Emscripten erneut installieren und konfigurieren kann.

    – Schäfer

    3. August 2018 um 13:38 Uhr

  • Sicher, aber ich dachte, vielleicht gibt es einen Weg, nur mit den Rostwerkzeugen, die sich gut in das Build-Skript integrieren lassen. Oder sieht es auch so aus, als könnte das wasm-bindgen-Projekt irgendwann etwas bewirken? Wie das Kompilieren der C-Lib zu WSAM, der Rust-Lib und dann das Generieren des JS-Glue-Codes, der sie verknüpft?

    – Olanod

    3. August 2018 um 13:48 Uhr

  • Ich habe auch einen Kommentar: Das Kleben von JavaScript kann schwierig sein, entweder weil man Speicher-Blobs zwischen den Aufrufen hin und her zuweisen und kopieren müsste (wenn die beiden Module ihren eigenen Speicher haben) oder sie einen sorgfältig entworfenen Zugriff auf einen gemeinsamen benötigen würden , importierter Speicher (damit er nicht durch Größenänderung oder auf andere Weise ungültig wird), den das sekundäre Modul überhaupt nicht initialisieren sollte (ohne weitere Magie würde sein Datenabschnitt mit Sicherheit mit dem Hauptmodul kollidieren).

    – Tevemadar

    12. August 2018 um 11:48 Uhr

  • Faule Version: Verwenden Sie corrode, um die C-Bibliothek in Rust umzuwandeln, und fügen Sie das in Ihre Kiste ein.

    – Valentin Lorentz

    19. August 2018 um 17:01 Uhr

Benutzeravatar von tevemadar
Tevemadar

TL;DR: Springe zu “Neue Woche, neue Abenteuer“, um “Hallo von C und Rust!”

Der nette Weg wäre, eine WASM-Bibliothek zu erstellen und sie an den Linker zu übergeben. rustc hat eine Option dafür (und es scheint auch Quellcode-Direktiven zu geben):

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

Der Trick ist, dass die Bibliothek eine Bibliothek sein muss, also muss sie enthalten reloc (und in der praxis linking) Abschnitte. Emscripten scheint dafür ein Symbol zu haben, 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 ist mit enthalten RELOCATABLEalso ist es nicht wirklich notwendig, ONLY_MY_CODE entfernt einige Extras, aber das spielt auch hier keine Rolle)

Die Sache ist, emcc nie ein verschiebbares generiert wasm Datei für mich, zumindest nicht die Version, die ich diese Woche heruntergeladen habe, für Windows (ich habe sie im Schwierigkeitsgrad „Schwer“ gespielt, was im Nachhinein vielleicht nicht die beste Idee war). Es fehlen also die Abschnitte und rustc beschwert sich immer wieder <something.wasm> is not a relocatable wasm file.

Dann kommt clangdie ein verschiebbares erzeugen kann wasm Modul mit einem sehr einfachen Einzeiler:

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

Dann rustc sagt “Linking subsection vorzeitig beendet”. Aw, ja (übrigens war mein Rust-Setup auch brandneu). Dann habe ich gelesen, dass es zwei sind clang wasm Ziele: wasm32-unknown-unknown-wasm und wasm32-unknown-unknown-elf, und vielleicht sollte letzteres hier verwendet werden. Wie meine auch ganz neu llvm+clang build läuft bei diesem Ziel auf einen internen Fehler und bittet mich, einen Fehlerbericht an die Entwickler zu senden, es könnte etwas sein, das auf einfach oder mittel getestet werden kann, wie auf einer *nix- oder Mac-Box.

Minimale Erfolgsgeschichte: Summe aus drei Zahlen

An dieser Stelle habe ich nur hinzugefügt lld zu llvm und erfolgreich mit dem manuellen Verknüpfen eines Testcodes aus Bitcode-Dateien:

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

Oh ja, es summiert Zahlen, 2 Zoll C und 1+2 in Rost:

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>

Dass _ZN4core9panicking5panic17hfbb77505dc622acdE fühlt sich sehr natürlich an (das Modul wird in zwei Schritten kompiliert und instanziiert, um die Exporte und Importe zu protokollieren, so können solche fehlenden Teile gefunden werden), und prognostiziert das Ende dieses Versuchs: Das Ganze funktioniert, weil es gibt kein anderer Verweis auf die Laufzeitbibliothek, und diese bestimmte Methode könnte manuell verspottet/bereitgestellt werden.

Nebengeschichte: Schnur

Wie alloc und sein Layout Ding hat mich ein wenig erschreckt, ich bin mit dem vektorbasierten Ansatz gegangen, der von Zeit zu Zeit beschrieben / verwendet wird, zum Beispiel hier oder weiter Hallo Rost!.
Hier ist ein Beispiel, wie man die Zeichenfolge “Hello from …” von außen erhält …

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
}

Gebaut als rustc rhello.rs --target wasm32-unknown-unknown --crate-type=cdylib

… und tatsächlich mit arbeiten 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>

Es ist nicht besonders schön (eigentlich habe ich keine Ahnung von Rust), aber es tut etwas, was ich von ihm erwarte, und sogar das dealloc könnte funktionieren (zumindest löst ein zweimaliger Aufruf Panik aus).
Es gab eine wichtige Lektion auf dem Weg: Wenn das Modul seinen Speicher verwaltet, kann sich seine Größe ändern, was dazu führt, dass die Sicherung ungültig wird ArrayBuffer Objekt und seine Ansichten. Deshalb also memory.buffer wird mehrfach geprüft und geprüft nach hineinrufen wasm Code.

Und hier stecke ich fest, weil dieser Code auf Laufzeitbibliotheken verweisen würde, und .rlib-s. Das, was ich einem manuellen Build am nächsten kommen könnte, ist das Folgende:

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

Wo musste ich die verwenden lld sitzt in den Tiefen der Rust-Toolchain als .rlib-s sollen sein interpretiertalso sind sie an die gebunden Rust Werkzeugkette

--crate-type=rlib, #[crate_type = "rlib"] – Es wird eine „Rostbibliothek“-Datei erstellt. Dies wird als Zwischenartefakt verwendet und kann als “statische Rust-Bibliothek” betrachtet werden. Diese rlib Dateien, im Gegensatz zu staticlib Dateien, werden vom Rust-Compiler bei der zukünftigen Verknüpfung interpretiert. Damit ist im Wesentlichen gemeint rustc sucht nach Metadaten in rlib Dateien so, wie es nach Metadaten in dynamischen Bibliotheken aussieht. Diese Form der Ausgabe wird verwendet, um statisch verknüpfte ausführbare Dateien sowie staticlib Ausgänge.

Natürlich das lld frisst die nicht .wasm/.o Dateien erzeugt mit clang oder llc (“Linking Subsection vorzeitig beendet”), vielleicht sollte auch der Rust-Teil mit dem Brauch nachgebaut werden llvm.
Außerdem scheinen diesem Build die eigentlichen Allokatoren zu fehlen chellogibt es 4 weitere Einträge in der Importtabelle: __rust_alloc, __rust_alloc_zeroed, __rust_dealloc und __rust_realloc. Was tatsächlich von JavaScript bereitgestellt werden könnte, vereitelt nur die Idee, Rust seinen eigenen Speicher verwalten zu lassen, außerdem war im Single-Pass ein Allocator vorhanden rustc bauen … Oh ja, hier habe ich für diese Woche aufgegeben (11. August 2018 um 21:56 Uhr)

Neue Woche, neue Abenteuer, mit Binaryen, wasm-dis/merge

Die Idee war, den vorgefertigten Rust-Code zu modifizieren (mit Zuweisungen und allem, was vorhanden ist). Und dieser funktioniert. Solange Ihr C-Code keine Daten enthält.

Proof-of-Concept-Code:

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;
}

Nicht sehr üblich, aber es ist C-Code.

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 ist die gleiche, die in “Nebengeschichte: Zeichenfolge” vorgestellt wird)
Und das Ergebnis funktioniert wie

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>

Sogar die Allokatoren scheinen etwas zu tun (ptr Messwerte aus wiederholten Blöcken mit/ohne dealloc zeigen, wie der Speicher entsprechend nicht leckt / leckt).

Natürlich ist das super zerbrechlich und hat auch mysteriöse Teile:

  • wenn die endgültige Zusammenführung mit ausgeführt wird -S switch (erzeugt Quellcode statt .wasm), und die Ergebnis-Assembly-Datei wird separat kompiliert (mit wasm-as), wird das Ergebnis ein paar Bytes kürzer sein (und diese Bytes befinden sich irgendwo in der Mitte des laufenden Codes, nicht in den Export-/Import-/Datenabschnitten).
  • Die Reihenfolge der Zusammenführung ist wichtig, Datei mit “Rost-Herkunft” muss zuerst kommen. wasm-merge chello.wast rhello.wast [...] stirbt mit einer unterhaltsamen Botschaft

    [wasm-validator error in module] unerwartet falsch: Segment-Offset sollte angemessen sein, on
    [i32] (i32.const 1)
    Schwerwiegend: Fehler beim Überprüfen der Ausgabe

  • wahrscheinlich meine Schuld, aber ich musste eine komplette bauen chello.wasm Modul (also mit Verlinkung). Nur kompilieren (clang -c [...]) führte zu dem verschiebbaren Modul, das ganz am Anfang dieser Geschichte so sehr vermisst wurde, aber das Dekompilieren dieses Moduls (zu .wast) hat den benannten Export verloren (chello()):
    (export "chello" (func $chello)) verschwindet vollständig
    (func $chello ... wird (func $0 ...eine interne Funktion (wasm-dis verliert reloc und linking Abschnitten, indem Sie nur eine Bemerkung über sie und ihre Größe in die Assemblierungsquelle einfügen)
  • verwandt mit dem vorherigen: Auf diese Weise (Bau eines vollständigen Moduls) können Daten aus dem sekundären Modul nicht verschoben werden wasm-merge: während es eine Möglichkeit gibt, Verweise auf die Zeichenfolge selbst abzufangen (const char *HELLO="Hello from C"; wird insbesondere bei Offset 1024 eine Konstante und wird später als bezeichnet (i32.const 1024) wenn es eine lokale Konstante innerhalb einer Funktion ist), passiert es nicht. Und wenn es sich um eine globale Konstante handelt, wird ihre Adresse ebenfalls zu einer globalen Konstante, die Nummer 1024 wird bei Offset 1040 gespeichert, und auf die Zeichenfolge wird verwiesen (i32.load offset=1040 [...]die schwer zu fangen beginnt.

Zum Lachen, dieser Code kompiliert und funktioniert auch …

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);
}

… es schreibt einfach “Hello from C” in die Mitte von Rusts Nachrichtenpool, was den Ausdruck ergibt

Hallo von Clt::unwrap()` zu einem `Err`an value und Rust!

(Erklärung: 0-Initialisierer sind im neu kompilierten Code wegen des Optimierungs-Flags nicht vorhanden, -O)
Und es wirft auch die Frage nach der Lokalisierung von a auf libc (obwohl sie ohne definiert werden my_, clang erwähnt strlen und strcpy als Einbauten, die auch ihre korrekten Signaturen angeben, gibt es keinen Code für sie aus und sie werden zu Importen für das resultierende Modul).

  • Nebenbefund falls jemand folgt: clang --target=wasm32-unknown-unknown-elf [...] stirbt auch unter Linux mit einem internen Compiler-Fehler.

    – Tevemadar

    14. August 2018 um 21:47 Uhr

1394790cookie-checkWie verwende ich eine C-Bibliothek in einer zu WebAssembly kompilierten Rust-Bibliothek?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy