Extend wasmtime C API for wasmtime-go

Recently I have been working on wasmtime C API, trying to add some features that exists in its Rust API but not in C API, where the latter is supporting other languages’ bindings, e.g., wasmtime-go. I found the online resources aren’t too helpful and it took me sometime to figure it out how to test the Windows build locally.

Requirements

  • cargo 1.72.0 or later
  • rustc 1.72.0 or later
  • go 1.20 or later
  • A clone of wasmtime repository
  • A clone of wasmtime-go repository

Develop wasmtime C API for different platforms

The feature I am developing is unfortunately very platform-specific, so I have to conditionally compile its implementation for different platforms.

And basically this is what I did:

use std::ffi::c_void;
use std::fs::File;
#[cfg(unix)]
use std::os::fd::FromRawFd;
#[cfg(windows)]
use std::os::windows::{io::FromRawHandle, raw::HANDLE};

Later in the code, I also had to declare the same function for three times:

#[cfg(unix)]
#[no_mangle]
pub unsafe extern "C" fn wasmtime_context_insert_file(
    context: CStoreContextMut,
    guest_fd: u32,
    host_fd: *mut c_void, // i32
    access_mode: u32,
) {
    // ...
}

#[cfg(windows)]
#[no_mangle]
pub unsafe extern "C" fn wasmtime_context_insert_file(
    context: CStoreContextMut,
    guest_fd: u32,
    host_fd: *mut c_void, // HANDLE
    access_mode: u32,
) {
    // ...
}

#[cfg(not(any(unix, windows)))]
#[no_mangle]
pub unsafe extern "C" fn wasmtime_context_insert_file(
    context: CStoreContextMut,
    guest_fd: u32,
    host_fd: *mut c_void, // i32
    access_mode: u32,
) {
    unimplemented!("wasmtime_context_insert_file")
}

Since I managed to make the signature for the function the same for all platforms, the C header file will require only one single function declaration. That’s lucky, because I sincerely don’t know how to declare a function with different signatures for different platforms in C header file – I don’t know what’s the correct #tag for that.

WASM_API_EXTERN void wasmtime_context_insert_file(wasmtime_context_t *context, uint32_t guest_fd, void *host_fd, uint32_t access_mode);

Compiling

It is quite straightforward to compile the library for different platforms. Or… at least it looks like it.

DON’T TRY THIS AT HOME, UNLESS YOU HAVE FINISHED READING THIS ARTICLE.

Linux

Compile for release:

cargo build --release -p wasmtime-c-api

It produces in wasmtime/target/release a libwasmtime.a, which is the static library for linux/macOS platforms.

If you want to compile for debug, you can do:

cargo build -p wasmtime-c-api

The compilation output will be in wasmtime/target/debug.

Windows

The compilation command is the same as Linux.

cargo build --release -p wasmtime-c-api

It produces in wasmtime/target/release three target files:

  • wasmtime.dll
  • wasmtime.dll.lib
  • wasmtime.lib

Just as the content in the pre-compiled release package, wasmtime-{version}-x86_64-windows-c-api.zip.

Testing

It is non-trivial to test C API with only itself. You will need a caller. Therefore I will test it with wasmtime-go. Anyways the changes I am contributing to wasmtime C API is for the sake of wasmtime-go, so it makes sense to test it with wasmtime-go.

wasmtime-go is released with compiled wasmtime libraries for different platforms, and C header files. However, since we manually compiled them, we will need to manually put them into the correct place.

Linux

Just run wasmtime-go/ci/local.sh.

./ci/local.sh /path/to/your/fork/of/wasmtime

It will copy libwasmtime.a from wasmtime/target/release (or wasmtime/target/debug if release doesn’t exist) to wasmtime-go/build/{os-arch} (for example, wasmtime-go/build/linux-x86_64), along with linking (not copying, note this) all the header files (.h) in wasmtime/crates/c-api/include to wasmtime-go/build/include.

This should work for macos as well, but I haven’t tested it. You may sponsor me a macbook if you want me to test it on macos ;)

Windows

This is the tricky part.

You will need to manually copy wasmtime.dll, wasmtime.dll.lib and wasmtime.lib to wasmtime-go/build/windows-x86_64. And C header files to wasmtime-go/build/include.

Now things seems to be on the same page.

Run go test with your implemented test, boom:

corrupt .drectve at end of def file

You will get a bunch of error messages and the beginning of the error message is corrupt .drectve at end of def file. What does that even mean?

Let’s check what’s in the released wasmtime-go, and what does wasmtime-go/ci/download-wasmtime.py pull on Pull Requests.

In the released wasmtime-go (v12.0.0), I see only libwasmtime.a. And the Pull Request CI actually downloads wasmtime-{version}-x86_64-mingw-c-api.zip from wasmtime, which includes 3 files:

  • libwasmtime.a
  • libwasmtime.dll.a
  • wasmtime.dll

This isn’t good. I do not have libwasmtime.dll.a in our local build. What happened? So it turns out that my default toolchain is from msvc, but the CI is using mingw, or in other name, gnu.

Install the new toolchain

rustup toolchain install stable-x86_64-pc-windows-gnu

And in .rustup/settings.toml, change the default toolchain to stable-x86_64-pc-windows-gnu:

default_toolchain = "stable-x86_64-pc-windows-gnu"

Compile with the new toolchain

Again

cargo build --release -p wasmtime-c-api

It produces in wasmtime/target/release three target files:

  • libwasmtime.a
  • libwasmtime.dll.a
  • wasmtime.dll

Copy the new files to wasmtime-go

Now copy libwasmtime.a, libwasmtime.dll.a and wasmtime.dll to wasmtime-go/build/windows-x86_64. And C header files to wasmtime-go/build/include.

Run the test

Run go test with your implemented test. Then if everything goes well, you will get

exit status 0xc0000135

WHY? Okay, it turns out the wasmtime.dll we compiled cannot be found by wasmtime-go. Adding the path to wasmtime.dll to PATH environment variable solves the problem.

Remember to restart your terminal!

And then go test finally passes.

Side note: release with only libwasmtime.a

The wasmtime-go is released with only libwasmtime.a not even any dll. However when I first put only libwasmtime.a in wasmtime-go/build/windows-x86_64, go test will pass but produce multiple warnings including

corrupt .drectve at end of def file

Haven’t figured out why yet. It seems to be an issue with the linker for CGO.