Как использовать библиотеку C в библиотеке Rust, скомпилированной в WebAssembly?

Я экспериментирую с Rust, WebAssembly и C совместимостью, чтобы в конечном итоге использовать Rust (со статической зависимостью C) в браузере или Node.js. Я использую wasm-bindgen для кода 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())
}

Мой первый наивный подход состоял в том, чтобы иметь скрипт build.rs который использует ящик gcc для создания статической библиотеки из кода C. Прежде чем вводить бит WASM, я мог бы скомпилировать программу Rust и посмотреть hello from C вывода hello from C в консоли, теперь я получаю сообщение об ошибке от компилятора, говорящего

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

Это имеет смысл, теперь, когда я думаю об этом, поскольку файл hello.o был скомпилирован для моей архитектуры ноутбука, а не для WebAssembly.

В идеале я хотел бы, чтобы это работало из коробки, добавляя в мой build.rs некоторые магии, которые, например, скомпилировали бы библиотеку C как статическую библиотеку WebAssembly, которую может использовать Rust.

Я думаю, что это могло бы работать, но хотелось бы избежать, поскольку это звучит более проблематично, использует Emscripten для создания библиотеки WASM для кода C, а затем компилирует библиотеку Rust отдельно и склеивает их вместе в JavaScript.

Ответ 1

TL; DR: перейти к " Новая неделя, новые приключения ", чтобы получить "Привет от C и Rust!".

Хорошим способом было бы создать библиотеку WASM и передать ее компоновщику. rustc есть опция для этого (и, похоже, существуют также директивы ode -c ode):

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

Хитрость заключается в том, что библиотека должна быть библиотека, поэтому она должна содержать reloc (и на практике linking) секций. Emscripten, кажется, имеет для этого символ, 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 включен в RELOCATABLE, поэтому это не обязательно, ONLY_MY_CODE снимает некоторые дополнительные функции, но здесь тоже неважно)

Дело в том, что emcc никогда не создавал для меня перемещаемый файл wasm, по крайней мере, не версию, которую я загрузил на этой неделе, для Windows (я играл на этом с трудной трудностью, что ретроспективно могло бы быть не лучшей идеей). Таким образом, разделы отсутствуют, и rustc продолжает жаловаться на <something.wasm> is not a relocatable wasm file.

Затем идет clang, который может генерировать перемещаемый модуль wasm с очень простым wasm:

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

Затем rustc говорит: "Связывание суб -s ection закончилось преждевременно". Aw, да (кстати, моя установка Rust была совершенно новой). Потом я прочитал, что есть два clang wasm цели: wasm32-unknown-unknown-wasm и wasm32-unknown-unknown-elf, и, возможно, последние из них следует использовать здесь. Поскольку моя новая марка llvm+clang запускает внутреннюю ошибку с этой целью, прося меня отправить отчет об ошибках разработчикам, это может быть что-то испытать на простом или среднем, например, в некоторых * nix или Mac.

Минимальная история успеха: сумма трех чисел

На данный момент я просто добавил lld в llvm и смог связать тестовый код вручную из файлов биткода:

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

Да, он суммирует числа, 2 в C и 1 + 2 в 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>

То, что _ZN4core9panicking5panic17hfbb77505dc622acdE чувствует себя очень естественным (модуль скомпилирован и создан в два этапа, чтобы регистрировать экспорт и импорт, то есть как можно найти такие недостающие фрагменты) и прогнозирует кончину этой попытки: все работает, потому что нет другой ссылки на библиотеку времени выполнения, и этот конкретный метод может быть изделен/предоставлен вручную.

Боковая история: строка

Поскольку alloc и его Layout меня немного пугали, я пошел с основанным на векторе подходом, который использовался/использовался время от времени, например здесь или по Hello, Rust! ,
Вот пример, получивший строку "Hello from..." извне...

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
}

Построено как rustc rhello.rs --target wasm32-unknown-unknown --crate-type=cdylib

... и фактически работает с 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>

Это не особенно красиво (на самом деле я понятия не имею о Rust), но он делает то, что я ожидаю от него, и даже этот dealloc может работать (по крайней мере, он дважды вызывает панику).
На этом был важный урок: когда модуль управляет своей памятью, его размер может измениться, что приводит к аннулированию объекта ArrayBuffer и его представлений. Вот почему memory.buffer проверяется несколько раз и проверяется после вызова в код wasm.

И здесь я застрял, потому что этот код будет ссылаться на библиотеки времени выполнения и .rlib -s. Самый близкий, который я мог бы получить к ручной сборке, следующий:

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

Где я должен был использовать lld сидит в глубине инструментария, ржавчины, как .rlib, как говорят, быть истолкован, таким образом, они связаны с Rust набора инструментов

--crate-type=rlib, #[crate_type = "rlib"] - Будет создан файл библиотеки Rust. Это используется как промежуточный артефакт и может рассматриваться как "статическая библиотека Rust". Эти файлы rlib, в отличие от файлов staticlib, интерпретируются компилятором Rust в будущем соединении. Это означает, что rustc будет искать метаданные в файлах rlib например, ищет метаданные в динамических библиотеках. Эта форма вывода используется для создания статически связанных исполняемых файлов, а также staticlib выходов staticlib.

Конечно, этот lld не ест файлы .wasm/.o сгенерированные с clang или llc ("Связывание суб -s ection закончилось преждевременно"), возможно, часть Rust также должна быть перестроена с помощью пользовательского llvm.
Кроме того, эта сборка, по-видимому, отсутствует у фактических распределителей, кроме chello, в таблице импорта будет еще 4 записи: __rust_alloc, __rust_alloc_zeroed, __rust_dealloc и __rust_realloc. Что на самом деле может быть предоставлено от JavaScript в конце концов, просто побеждает идею позволить Rust обрабатывать свою собственную память, плюс распределитель присутствует в однопроходной rustc... О, да, именно здесь я отказался от этого неделю (11 августа 2018 года, в 21:56)

Новая неделя, новые приключения, с Binaryen, wasm-dis/merge

Идея заключалась в том, чтобы модифицировать готовый код Rust (наличие распределителей и все на своем месте). И это работает. Пока ваш код C не имеет данных.

Доказательство кода концепции:

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

Не очень обычный, но это код 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 - это то же самое, что представлено в "Side story: string")
И результат работает как

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>

Даже распределители, похоже, что-то делают (показания ptr из повторных блоков с/без dealloc показывают, как память не течет/течет соответственно).

Конечно, это супер-хрупкое и имеет таинственные части:

  • если окончательное слияние выполняется с -s переключателя -s (генерирует исходный код вместо .wasm), а файл сборки результата компилируется отдельно (используя wasm-as), результат будет на пару байтов короче (и эти байты где-то в самой середине исполняемого кода, а не в разделах export/import/data)
  • порядок слияния, файл с "Rust -O rigin" должен быть первым. wasm-merge chello.wast rhello.wast [...] умирает с развлекательным сообщением

    [wasm-validator error in module] неожиданное ложное: смещение сегмента должно быть разумным, on
    [i32] (i32.const 1)
    Fatal: ошибка в подтверждении вывода

  • вероятно, моя вина, но мне пришлось построить полный модуль chello.wasm (так, со chello.wasm). Только компиляция (clang -c [...]) привела к перемещаемому модулю, который так сильно пропустил в самом начале этой истории, но декомпилировал, что один (на .wast) потерял именованный экспорт (chello()):
    (export "chello" (func $chello)) полностью исчезает
    (func $chello... становится (func $0..., внутренняя функция (wasm-dis теряет reloc и linking разделы, помещая только замечание о них и их размер в источник сборки)
  • связанный с предыдущим: этот способ (создание полного модуля) данных из вторичного модуля не может быть перемещен с помощью wasm-merge: пока есть возможность поймать ссылки на самую строку (const char *HELLO="Hello from C"; становится константой со смещением 1024 в частности и позже упоминается как (i32.const 1024) если это локальная константа внутри функции), этого не происходит. И если это глобальная константа, ее адрес также становится глобальной константой, номер 1024, хранящийся со смещением 1040, и строка будет называться (i32.load offset=1040 [...], которая начинает трудно ловить.

Для смеха этот код компилируется и работает тоже...

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

... только он пишет "Hello from C" в середине пула сообщений Rust, что приводит к распечатке

Привет от Clt :: unwrap() 'по значению Err'an и Rust!

(Объяснение: 0-инициализаторы отсутствуют в перекомпилированном коде из-за флага оптимизации, -O)
И он также поднимает вопрос о размещении libc (хотя они определяют их без my_, clang упоминает strlen и strcpy как встроенные модули, также сообщая их правильные singatures, он не испускает код для них, и они становятся импортом для полученного модуля),