Skip to content

Commit 17aa112

Browse files
feat: initial wasm c-api impl
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent b45a98a commit 17aa112

28 files changed

Lines changed: 3585 additions & 0 deletions

‎.github/workflows/release.yaml‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,73 @@ on:
33
push:
44
tags: ["v*"]
55

6+
permissions:
7+
contents: read
8+
69
jobs:
10+
artifacts:
11+
name: Build ${{ matrix.target }}
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
include:
17+
- os: ubuntu-22.04
18+
target: x86_64-unknown-linux-gnu
19+
shared-library: libtinywasm.so
20+
- os: ubuntu-22.04-arm
21+
target: aarch64-unknown-linux-gnu
22+
shared-library: libtinywasm.so
23+
- os: macos-15-intel
24+
target: x86_64-apple-darwin
25+
shared-library: libtinywasm.dylib
26+
- os: macos-15
27+
target: aarch64-apple-darwin
28+
shared-library: libtinywasm.dylib
29+
steps:
30+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
31+
with:
32+
persist-credentials: false
33+
- uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
34+
with:
35+
toolchain: stable
36+
target: ${{ matrix.target }}
37+
rustflags: ""
38+
- name: Build CLI and C API
39+
env:
40+
TARGET: ${{ matrix.target }}
41+
run: |
42+
cargo build --locked --release --target "$TARGET" -p tinywasm-cli
43+
if [[ "$TARGET" == *-apple-darwin ]]; then
44+
cargo rustc --locked --release --target "$TARGET" -p tinywasm-c-api -- \
45+
-C link-arg=-Wl,-install_name,@rpath/libtinywasm.dylib
46+
else
47+
cargo build --locked --release --target "$TARGET" -p tinywasm-c-api
48+
fi
49+
- name: Package and smoke-test
50+
env:
51+
TARGET: ${{ matrix.target }}
52+
SHARED_LIBRARY: ${{ matrix.shared-library }}
53+
run: |
54+
name="tinywasm-${GITHUB_REF_NAME}-${TARGET}"
55+
package="$RUNNER_TEMP/$name"
56+
mkdir -p "$package/bin" "$package/lib" "$package/include" "$package/licenses" dist
57+
cp "target/$TARGET/release/tinywasm" "$package/bin/"
58+
cp "target/$TARGET/release/$SHARED_LIBRARY" "target/$TARGET/release/libtinywasm.a" "$package/lib/"
59+
cp crates/c-api/include/*.h "$package/include/"
60+
cp LICENSE-MIT LICENSE-APACHE crates/c-api/include/LICENSE-wasm-c-api "$package/licenses/"
61+
cp crates/c-api/README.md "$package/README.md"
62+
"$package/bin/tinywasm" --version
63+
cc -std=c11 -I"$package/include" crates/c-api/examples/add.c \
64+
-L"$package/lib" -Wl,-rpath,"$package/lib" -ltinywasm -o "$RUNNER_TEMP/c-api-example"
65+
"$RUNNER_TEMP/c-api-example"
66+
tar -czf "dist/$name.tar.gz" -C "$RUNNER_TEMP" "$name"
67+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
68+
with:
69+
name: tinywasm-${{ matrix.target }}
70+
path: dist/*.tar.gz
71+
if-no-files-found: error
72+
773
publish:
874
runs-on: ubuntu-latest
975
environment: release
@@ -19,3 +85,28 @@ jobs:
1985
- run: cargo publish --workspace
2086
env:
2187
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
88+
89+
release:
90+
name: Attach release archives
91+
needs: [publish, artifacts]
92+
runs-on: ubuntu-latest
93+
permissions:
94+
contents: write
95+
steps:
96+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
97+
with:
98+
pattern: tinywasm-*
99+
merge-multiple: true
100+
path: dist
101+
- name: Create checksums and upload
102+
working-directory: dist
103+
env:
104+
GH_TOKEN: ${{ github.token }}
105+
GH_REPO: ${{ github.repository }}
106+
TAG: ${{ github.ref_name }}
107+
run: |
108+
sha256sum -- *.tar.gz > SHA256SUMS
109+
if ! gh release view "$TAG" > /dev/null 2>&1; then
110+
gh release create "$TAG" --verify-tag --generate-notes
111+
fi
112+
gh release upload "$TAG" ./*.tar.gz SHA256SUMS --clobber

‎.github/workflows/test.yaml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ permissions:
1212
contents: read
1313

1414
jobs:
15+
c-api:
16+
name: C API
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
20+
with:
21+
persist-credentials: false
22+
- uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
23+
with:
24+
toolchain: nightly
25+
components: miri
26+
rustflags: ""
27+
- run: cargo miri test -p tinywasm-c-api --lib
28+
- run: make -C crates/c-api test
29+
- run: make -C crates/c-api test TINYWASM_C_API_PREFIX=test_ TARGET_DIR=../../target/c-api-prefixed
30+
1531
build-wasm:
1632
name: Build wasm
1733
runs-on: ubuntu-latest

‎Cargo.lock‎

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎README.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ assert_eq!(result, 3);
4343

4444
See the [examples](./examples) directory and [documentation](https://docs.rs/tinywasm) for more information.
4545

46+
### C and C++
47+
48+
The experimental [`tinywasm-c-api` crate](./crates/c-api) provides `wasm.h`,
49+
TinyWasm extensions in `tinywasm.h`, and static and shared libraries. Build the
50+
libraries with:
51+
52+
```sh
53+
make -C crates/c-api
54+
```
55+
56+
See its [README](./crates/c-api/README.md) for installation and usage.
57+
4658
## Cargo Features
4759

4860
- **`full`:** Enables `archive`, `debug`, `parallel-parser`, `parser`, `state`, and `validate`. Enabled by default.

‎crates/c-api/Cargo.toml‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "tinywasm-c-api"
3+
version.workspace = true
4+
edition.workspace = true
5+
rust-version.workspace = true
6+
description = "WebAssembly C API for TinyWasm"
7+
repository.workspace = true
8+
license.workspace = true
9+
publish = false
10+
11+
[lib]
12+
crate-type = ["cdylib", "staticlib"]
13+
name = "tinywasm"
14+
15+
[dependencies]
16+
tinywasm = { workspace = true, features = ["archive", "parser", "std", "validate"] }
17+
18+
[dev-dependencies]
19+
wat.workspace = true
20+
21+
[features]
22+
custom-prefix = []
23+
24+
[lints]
25+
workspace = true

‎crates/c-api/Makefile‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Optional convenience targets. Cargo remains the library build system.
2+
CARGO ?= cargo
3+
CC ?= cc
4+
CXX ?= c++
5+
PREFIX ?= /usr/local
6+
DESTDIR ?=
7+
LIBDIR ?= $(PREFIX)/lib
8+
PROFILE ?= release
9+
TARGET_DIR ?= $(if $(CARGO_TARGET_DIR),$(CARGO_TARGET_DIR),../../target)
10+
BUILD_LIBDIR := $(abspath $(TARGET_DIR)/$(if $(filter dev,$(PROFILE)),debug,$(PROFILE)))
11+
BUILDDIR := $(abspath $(TARGET_DIR)/c-api)
12+
CARGO_FLAGS := --profile $(PROFILE)
13+
ifneq ($(TINYWASM_C_API_PREFIX),)
14+
export TINYWASM_C_API_PREFIX
15+
CARGO_FLAGS += --features custom-prefix
16+
CPPFLAGS += -DTINYWASM_C_API_PREFIX=$(TINYWASM_C_API_PREFIX)
17+
endif
18+
CFLAGS ?= -O2 -Wall -Wextra -Werror
19+
UNAME := $(shell uname -s)
20+
SHARED_EXT := $(if $(filter Darwin,$(UNAME)),dylib,so)
21+
# Native libraries reported by `cargo rustc -- --print native-static-libs`.
22+
NATIVE_LIBS ?= $(if $(filter Darwin,$(UNAME)),-framework Security -framework CoreFoundation -liconv -lSystem,-lgcc_s -lutil -lrt -lpthread -lm -ldl -lc)
23+
VERSION := $(shell sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' ../../Cargo.toml)
24+
25+
.PHONY: all build example test install
26+
all: build
27+
28+
build:
29+
$(CARGO) build -p tinywasm-c-api $(CARGO_FLAGS) --target-dir "$(abspath $(TARGET_DIR))"
30+
31+
example: build
32+
mkdir -p "$(BUILDDIR)"
33+
$(CC) $(CPPFLAGS) $(CFLAGS) -std=c11 -Iinclude examples/add.c -L"$(BUILD_LIBDIR)" -Wl,-rpath,"$(BUILD_LIBDIR)" -ltinywasm $(LDFLAGS) -o "$(BUILDDIR)/add"
34+
"$(BUILDDIR)/add"
35+
36+
test: example
37+
$(CXX) $(CPPFLAGS) -std=c++17 -Wall -Wextra -Werror -fsyntax-only -x c++ -Iinclude include/tinywasm.h
38+
$(CARGO) run -p tinywasm-c-api --example fixtures --target-dir "$(abspath $(TARGET_DIR))" -- "$(BUILDDIR)"
39+
$(CC) $(CPPFLAGS) $(CFLAGS) -std=c11 -Iinclude tests/api.c -L"$(BUILD_LIBDIR)" -Wl,-rpath,"$(BUILD_LIBDIR)" -ltinywasm $(LDFLAGS) -o "$(BUILDDIR)/api"
40+
"$(BUILDDIR)/api" "$(BUILDDIR)"
41+
$(CC) $(CPPFLAGS) $(CFLAGS) -std=c11 -Iinclude examples/add.c "$(BUILD_LIBDIR)/libtinywasm.a" $(NATIVE_LIBS) $(LDFLAGS) -o "$(BUILDDIR)/add-static"
42+
"$(BUILDDIR)/add-static"
43+
$(CC) $(CPPFLAGS) $(CFLAGS) -std=c11 -Iinclude tests/api.c "$(BUILD_LIBDIR)/libtinywasm.a" $(NATIVE_LIBS) $(LDFLAGS) -o "$(BUILDDIR)/api-static"
44+
"$(BUILDDIR)/api-static" "$(BUILDDIR)"
45+
python3 tests/symbols.py "$(BUILD_LIBDIR)/libtinywasm.$(SHARED_EXT)" "$(CC)" "$(TINYWASM_C_API_PREFIX)"
46+
47+
install: build
48+
install -d "$(DESTDIR)$(PREFIX)/include" "$(DESTDIR)$(LIBDIR)/pkgconfig" "$(DESTDIR)$(PREFIX)/share/licenses/tinywasm"
49+
install -m644 include/wasm.h include/tinywasm.h include/tinywasm-prefix.h "$(DESTDIR)$(PREFIX)/include/"
50+
install -m644 "$(BUILD_LIBDIR)/libtinywasm.a" "$(BUILD_LIBDIR)/libtinywasm.$(SHARED_EXT)" "$(DESTDIR)$(LIBDIR)/"
51+
install -m644 include/LICENSE-wasm-c-api ../../LICENSE-APACHE ../../LICENSE-MIT "$(DESTDIR)$(PREFIX)/share/licenses/tinywasm/"
52+
sed -e 's|@PREFIX@|$(PREFIX)|g' -e 's|@LIBDIR@|$(LIBDIR)|g' -e 's|@VERSION@|$(VERSION)|g' -e 's|@NATIVE_LIBS@|$(NATIVE_LIBS)|g' -e 's|@PREFIX_FLAG@|$(if $(TINYWASM_C_API_PREFIX),-DTINYWASM_C_API_PREFIX=$(TINYWASM_C_API_PREFIX))|g' tinywasm.pc.in > "$(DESTDIR)$(LIBDIR)/pkgconfig/tinywasm.pc"

‎crates/c-api/README.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# TinyWasm C API
2+
3+
Experimental [WebAssembly C API](https://github.com/WebAssembly/wasm-c-api) support for TinyWasm.
4+
Use `wasm.h` to embed it in C or C++. See [Wasmtime's C API documentation](https://docs.wasmtime.dev/c-api/wasm_8h.html)
5+
for the standard interface and [`examples/add.c`](examples/add.c) for an example.
6+
7+
## Build
8+
9+
```sh
10+
make -C crates/c-api
11+
```
12+
13+
This builds `libtinywasm.so` (or `.dylib` on macOS) and `libtinywasm.a` in
14+
`target/release`. Headers are in `crates/c-api/include`. To install them and a
15+
`pkg-config` file, run `make -C crates/c-api install`. Run `make -C crates/c-api example` to
16+
build and run the C example.
17+
18+
## Notes
19+
20+
- Follow the ownership annotations in `wasm.h` and use the matching delete
21+
functions. Keep a store and its objects on the same thread.
22+
- Load `.wasm` bytes with `wasm_module_new`. Imports are positional, in module
23+
import order.
24+
- Include `tinywasm.h` for `tinywasm_last_error_message`, which copies the
25+
calling thread's last error into a vector you delete with `wasm_byte_vec_delete`.
26+
- Module imports and exports must have types representable in `wasm.h`. SIMD
27+
signatures, memory64/table64, GC references, and tags are not supported at
28+
the module boundary.
29+
30+
`wasm.h` is vendored from WebAssembly/wasm-c-api commit
31+
`9d6b93764ac96cdd9db51081c363e09d2d488b4d` under
32+
[`include/LICENSE-wasm-c-api`](include/LICENSE-wasm-c-api).

‎crates/c-api/build.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
fn main() {
2+
if std::env::var_os("CARGO_FEATURE_CUSTOM_PREFIX").is_some() {
3+
println!("cargo:rerun-if-env-changed=TINYWASM_C_API_PREFIX");
4+
if std::env::var_os("TINYWASM_C_API_PREFIX").is_none() {
5+
println!("cargo:rustc-env=TINYWASM_C_API_PREFIX=tinywasm_");
6+
}
7+
}
8+
}

‎crates/c-api/examples/add.c‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#include <stdio.h>
2+
#include "tinywasm.h"
3+
4+
int main(void) {
5+
/* (module (func (export "add") (param i32 i32) (result i32)
6+
* local.get 0 local.get 1 i32.add)) */
7+
const unsigned char binary[] = {
8+
0,97,115,109,1,0,0,0,1,7,1,96,2,127,127,1,127,
9+
3,2,1,0,7,7,1,3,97,100,100,0,0,10,9,1,7,0,32,0,32,1,106,11
10+
};
11+
wasm_engine_t* engine = wasm_engine_new();
12+
wasm_store_t* store = wasm_store_new(engine);
13+
wasm_byte_vec_t bytes;
14+
wasm_byte_vec_new(&bytes, sizeof(binary), (const char*)binary);
15+
wasm_module_t* module = wasm_module_new(store, &bytes);
16+
wasm_byte_vec_delete(&bytes);
17+
if (!module) {
18+
wasm_message_t error;
19+
tinywasm_last_error_message(&error);
20+
fprintf(stderr, "%s\n", error.data);
21+
wasm_byte_vec_delete(&error);
22+
wasm_store_delete(store);
23+
wasm_engine_delete(engine);
24+
return 1;
25+
}
26+
27+
wasm_extern_vec_t imports = WASM_EMPTY_VEC;
28+
wasm_trap_t* trap = NULL;
29+
wasm_instance_t* instance = wasm_instance_new(store, module, &imports, &trap);
30+
assert(instance && !trap);
31+
wasm_extern_vec_t exports;
32+
wasm_instance_exports(instance, &exports);
33+
wasm_val_t arguments[] = { WASM_I32_VAL(20), WASM_I32_VAL(22) };
34+
wasm_val_t result[1];
35+
wasm_val_vec_t args = WASM_ARRAY_VEC(arguments);
36+
wasm_val_vec_t results = WASM_ARRAY_VEC(result);
37+
trap = wasm_func_call(wasm_extern_as_func(exports.data[0]), &args, &results);
38+
assert(!trap);
39+
printf("20 + 22 = %d\n", result[0].of.i32);
40+
assert(result[0].of.i32 == 42);
41+
42+
wasm_extern_vec_delete(&exports);
43+
wasm_instance_delete(instance);
44+
wasm_module_delete(module);
45+
wasm_store_delete(store);
46+
wasm_engine_delete(engine);
47+
return 0;
48+
}

‎crates/c-api/examples/fixtures.rs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! Produces binary fixtures for the native C integration test.
2+
fn main() -> Result<(), Box<dyn std::error::Error>> {
3+
let directory = std::path::PathBuf::from(std::env::args_os().nth(1).expect("output directory"));
4+
std::fs::create_dir_all(&directory)?;
5+
let module = wat::parse_str(
6+
r#"
7+
(module
8+
(import "host" "same" (func $a (param i32) (result i32)))
9+
(import "host" "same" (func $b (param i32) (result i32)))
10+
(memory (export "memory") 1 2)
11+
(global (export "global") (mut i32) (i32.const 7))
12+
(table (export "table") 2 4 funcref)
13+
(func $inc (export "inc") (param i32) (result i32)
14+
local.get 0 i32.const 1 i32.add)
15+
(elem (i32.const 0) func $inc)
16+
(func (export "run") (param i32) (result i32)
17+
local.get 0 call $a local.get 0 call $b i32.add)
18+
(func (export "reenter") (param i32) (result i32) local.get 0 call $a)
19+
(func (export "trap_host") (param i32) (result i32) local.get 0 call $b)
20+
(func (export "trap") unreachable))
21+
"#,
22+
)?;
23+
std::fs::write(directory.join("api.wasm"), module)?;
24+
for (name, source) in [
25+
("simd", "(module (func (export \"v\") (result v128) v128.const i32x4 0 0 0 0))"),
26+
("memory64", "(module (memory (export \"m\") i64 1))"),
27+
("start", "(module (func $start unreachable) (start $start))"),
28+
] {
29+
std::fs::write(directory.join(format!("{name}.wasm")), wat::parse_str(source)?)?;
30+
}
31+
Ok(())
32+
}

0 commit comments

Comments
 (0)