wasm-c-api icon indicating copy to clipboard operation
wasm-c-api copied to clipboard

Avoid union to represent wasm::val

Open rossberg opened this issue 5 years ago • 2 comments

There are two problems with using a union to represent different Wasm values (especially in arrays):

  1. it wastes space,
  2. binary backwards-compatibility breaks the moment we need to add a wider value type (e.g. SIMD).

The latter could be circumvented by requiring an indirection for passing types wider than i64 or pointers, though that raises additional memory management questions.

If that's not an option that the latter might be a showstopper. For example, it might prevent Apple from officially supporting the API, because they have strict backwards-compatibility policies.

To address these issues, function arguments and results should not be passed as a homogeneous array-of-union, but as a heterogeneous list/tuple/struct.

Possible alternatives considered:

  • Use varargs. For example:

    auto Func::call(...) const -> own<Trap*>;
    

    The ... would be instantiated with the sequence of values with types corresponding to the function's parameters, followed by a sequence of out pointers corresponding to its results. Problems: (1) would not allow types with destructors, which we need to safely handle reference values in the C++ API; (2) varargs are out of reach for most FFI layers, so not a suitable option for the C API.

  • Use std::tuple. For example:

    template<class... T, class... R>
    auto Func::call(std::tuple<T...> args, std::tuple<R&...> results) const -> own<Trap*>;
    

    Problems: (1) does not address C API; (2) cannot be wrapped into C without copying.

  • A custom variant of tuple that supports standard layout. For example:

    template<class... T, class... R>
    auto Func::call(wasm::seq<T...> args, wasm::seq<R&...> results) const -> own<Trap*>;
    

    This would allow casting from and to a struct defined in C. Problems: (1) on the C side, may need to declare auxiliary structs per call site/type; (2) may be difficult for FFIs.

  • Pack into byte array. For example:

    auto Func::call(const byte* args, byte* results) const -> own<Trap*>;
    

    Problems: (1) does not safely handle types with destructors, e.g. reference values; (2) requires manual handling of alignment, not clear how in a platform-agnostic manner; (3) tedious to use.

  • Other solutions?

rossberg avatar Apr 24 '19 12:04 rossberg

Perhaps we can leave enough space for a v128 type, and rely on a pointer to allocated memory for anything larger that comes along. That would waste a lot of space, so we could also leave it pointer-sized, and use allocation for SIMD types.

binji avatar Apr 24 '19 19:04 binji

@binji, good point about using an indirection, I'll edit the issue to mention it. Maybe that is indeed good enough, though it still would be nicer to have a "naturally compact" representation.

rossberg avatar Apr 25 '19 06:04 rossberg