Skip to content

Interop with non-C ABIs in WebAssembly #16639

Open
@jamii

Description

@jamii

Currently, functions exported from zig use the clang ABI when compiling to wasm. This is perfect for interop with other languages that use the same ABI.

For interop with weirder targets, it would be useful to be able to import/export functions for any type signature that wasm supports, rather than just the subset used by the clang ABI.

Currently in zig it's not possible to import or export a function if the wasm type has:

The clang ABI also passes structs/arrays with more than one field by reference, so it's not possible in zig to pass structs by value in imported/exported functions. This is only an annoyance for params because we can manually unpack the fields in most case, but there is no workaround for returning structs by value.

This will affect interop with any non-C ABI (eg AssemblyScript can export functions that zig cannot import). I ran into this when writing a toy language runtime that passes around type-tagged pointers by value as (i32, i32):

const FatPointer = extern struct {
    tag: u32,
    ptr: *anyopaque,
};

export fn identity(p: FatPointer) FatPointer {
    return p;
}
> zig build-lib lib/runtime.zig -target wasm32-freestanding -mcpu generic+multivalue -
dynamic -rdynamic -O ReleaseSafe && wasm2wat -f runtime.wasm
(module
  (type (;0;) (func (param i32 i32)))
  (func $identity (type 0) (param i32 i32)
    (i64.store align=4
      (local.get 0)
      (i64.load align=4
        (local.get 1))))
  (memory (;0;) 16)
  (global $__stack_pointer (mut i32) (i32.const 1048576))
  (export "memory" (memory 0))
  (export "identity" (func $identity)))

The clang ABI here requires passing both input and output by reference instead of by value. As mentioned above, we can work around this for the input by manually unpacking it, but there is currently no way to opt in to using multivalue returns from zig.

The workaround available at the moment is to generate a wasm wrapper which reverses the loads/stores generated by the clang ABI.

;; Untested :)
(func $identity_wrapper (param i32 i32) (results i32 i32)
  ;; Store `p` to stack
  (i32.store offset=0 (local.get 0) (global.get $__stack_pointer))
  (i32.store offset=4 (local.get 1) (global.get $__stack_pointer))
  ;; Make space for `p` and result on stack
  (global.set $__stack_pointer (i32.sub (global.get $__stack_pointer) (i32.const  8))
  (call $identity 
    ;; Pointer to `p`
    (global.get $__stack_pointer)
    ;; Pointer to result
    (i32.sub (global.get $__stack_pointer) 4)))
  ;; Reset stack.
  (global.set $__stack_pointer (i32.add (global.get $__stack_pointer) (i32.const  8))
  ;; Return result.
  (i32.load offset=0 (global.get $__stack_pointer))
  (i32.load offset=4 (global.get $__stack_pointer)))

An ideal solution might look something like this:

const FatPointer = struct {
    tag: u32,
    ptr: *anyopaque,
};

export fn identity(p: FatPointer) callconv(.Wasm) FatPointer {
    return p;
}
> zig build-lib lib/runtime.zig -target wasm32-freestanding -mcpu generic+multivalue -
dynamic -rdynamic -O ReleaseSafe && wasm2wat -f runtime.wasm
(module
  (type (;0;) (func (param i32 i32) (result i32 i32)))
  (func $identity (type 0) (param i32 i32) (result i32 i32)
    (local.get 0)
    (local.get 1))
  (memory (;0;) 16)
  (global $__stack_pointer (mut i32) (i32.const 1048576))
  (export "memory" (memory 0))
  (export "identity" (func $identity)))

The rules for callconv(.Wasm) would be:

  • i32,i64,f32,f64 parameters are mapped to the same types in wasm.
  • Same for whatever zig types end up corresponding to vector and ref types, if any.
  • (Non-packed) structs and arrays are passed by value as multiple parameters.
  • If multivalue is not in the target feature set, using structs and arrays in the return type is a compile-time error.
  • Using any other type is a compile-time error.

Example:

// zig type
fn example(a: [2]FatPointer, b: v128) callconv(.Wasm) struct { f32, FatPointer } { ... }
;; wasm type
(func 
  (params 
    ;; a[0]
    i32 i32
    ;; a[1]
    i32 i32
    ;; b
    v128)
  (results
    ;; result[0]
    f32
    ;; result[1]
    i32 i32))

The goal would be to provide the minimum features necessary to allow other ABIs to be expressed in zig via comptime wrappers or codegen. (Eg emulating wasm-bindgen, implementing the wasm component ABI, or writing the runtime for a non-C-like language).

I'm not sure how much of this would have to be supported by upstream llvm. Eg is there bytecode you can emit to get multivalue results, or is it only available via the experimental-mv flag? In the worst case, it might be possible to polyfill by compiling with callconv(.C) and then generating wrappers like the one I wrote above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    arch-wasm32-bit and 64-bit WebAssemblyproposalThis issue suggests modifications. If it also has the "accepted" label then it is planned.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions