diff --git a/cmake/bindings.cmake b/cmake/bindings.cmake index 6715fdf68..edc5d02ec 100644 --- a/cmake/bindings.cmake +++ b/cmake/bindings.cmake @@ -152,7 +152,6 @@ function(wit_bindgen_edit p) COMMAND sed ${SED_INPLACE_ARGS} "'s_#include .wasi${p}\.h._#include \"wasi/wasi${p}.h\"_'" ${bottom_half}/sources/wasi${p}.c COMMAND sed ${SED_INPLACE_ARGS} "s/extern void exit_exit/_Noreturn extern void exit_exit/" ${bottom_half}/headers/public/wasi/__generated_wasi${p}.h COMMAND sed ${SED_INPLACE_ARGS} "s/extern void __wasm_import_exit_exit/_Noreturn extern void __wasm_import_exit_exit/" ${bottom_half}/sources/wasi${p}.c - COMMAND sed ${SED_INPLACE_ARGS} "s/__attribute__.*\"cabi_realloc\".*/#include \\\"cabi_realloc_augment.h\\\"/" ${bottom_half}/sources/wasi${p}.c DEPENDS bindings-${p} ) add_dependencies(bindings bindings-${p}-edit) diff --git a/libc-bottom-half/CMakeLists.txt b/libc-bottom-half/CMakeLists.txt index 13b68df8e..7ab1f2dce 100644 --- a/libc-bottom-half/CMakeLists.txt +++ b/libc-bottom-half/CMakeLists.txt @@ -184,10 +184,7 @@ if (WASI STREQUAL "p3") endif() if (HAVE_WASM_LIBCALL_THREAD_CONTEXT) - list(APPEND bottom_half_sources - sources/wasip3_tls.c - sources/__wasm_init_task.S - ) + list(APPEND bottom_half_sources sources/__wasm_task_hook.S) endif() # Don't export symbols of generated code in shared libraries, so specifically diff --git a/libc-bottom-half/crt/wasip3_symbol_references.h b/libc-bottom-half/crt/wasip3_symbol_references.h index b047054c4..e3ba96edd 100644 --- a/libc-bottom-half/crt/wasip3_symbol_references.h +++ b/libc-bottom-half/crt/wasip3_symbol_references.h @@ -7,12 +7,9 @@ #include // Force some symbols to be linked in for wasip3 -extern void __wasm_init_task(void); -extern void __wasm_init_async_task(void); +extern void __wasm_task_hook(uint32_t); extern void cabi_realloc(void); -__attribute__((used)) static void *__wasm_init_task_ref = __wasm_init_task; -__attribute__((used)) static void *__wasm_init_async_task_ref = - __wasm_init_async_task; +__attribute__((used)) static void *__wasm_task_hook_ref = __wasm_task_hook; __attribute__((used)) static void *cabi_realloc_ref = cabi_realloc; // Force `__wasm_{g,s}et_{stack_pointer,tls_base}` to exist as defined symbols. diff --git a/libc-bottom-half/sources/__cabi_realloc_wrapper.S b/libc-bottom-half/sources/__cabi_realloc_wrapper.S index 62de72b4b..e69de29bb 100644 --- a/libc-bottom-half/sources/__cabi_realloc_wrapper.S +++ b/libc-bottom-half/sources/__cabi_realloc_wrapper.S @@ -1,129 +0,0 @@ -#include - -#ifdef __wasm_libcall_thread_context__ -// The size, in bytes, of the stack statically allocated for `cabi_realloc`. -// This doesn't need to be too big as it's just running `cabi_realloc` as defined -// in wasi-libc. The current number is determined by: -// -// * Start with 16 bytes. -// * Double until tests don't crash in a debug build. -// * Double again. -// -// The goal is to be pretty modest since this is part of every module. -#define REALLOC_STACK_SIZE 512 - -// Declare the `__wasm_{g,s}et_stack_pointer` functions. -.functype __wasm_set_stack_pointer (i32) -> () -.functype __wasm_get_stack_pointer () -> (i32) -.type __wasm_set_stack_pointer,@function -.type __wasm_get_stack_pointer,@function -.functype __cabi_realloc_impl (i32, i32, i32, i32) -> (i32) -.type __cabi_realloc_impl,@function - -// Go ahead and generate an `-fPIC`-compatible addressing mode for -// `cabi_realloc_stack` so this is compatible with both PIC and not builds. -.type __memory_base,@global -.globaltype __memory_base, i32, immutable - -// For coop-threads builds we need to configure TLS to the initial TLS -// value for all tasks/etc, so import those symbols here. -// -// Note that this saves and restores context slot 1 itself rather than this -// module's TLS base. When several modules are linked together the slot holds an -// array of per-module base pointers, so clobbering just libc's entry would both -// leave the other modules pointing at the wrong TLS and scribble through -// whatever stale pointer the slot happened to contain. -#ifdef __wasi_cooperative_threads__ -.functype wasip3_context_set_1 (i32) -> () -.functype wasip3_context_get_1 () -> (i32) -.functype __wasilibc_set_task_tls (i32) -> () -.type wasip3_context_set_1,@function -.type wasip3_context_get_1,@function -.type __wasilibc_set_task_tls,@function -.import_module wasip3_context_get_1, "$root" -.import_name wasip3_context_get_1, "[context-get-1]" -.import_module wasip3_context_set_1, "$root" -.import_name wasip3_context_set_1, "[context-set-1]" -.type __init_tls_base,@global -#ifdef __PIC__ -.globaltype __init_tls_base, i32 -#else -.globaltype __init_tls_base, i32, immutable -#endif -#endif - -// This wrapper takes over the `cabi_realloc` symbol itself and the -// `cabi_realloc` core module export. The C implementation it forwards to is -// renamed to `__cabi_realloc_impl` by `cabi_realloc_augment.h`. -// -// Note that it's important that the symbol name matches the export name here. -// In a `BUILD_SHARED=ON` build `libc.so` is dynamically linked against by other -// shared libraries and by `crt1-{command,reactor}.o`, and the name a symbol is -// resolved by in that context is its wasm export name. If `.export_name` were -// used to rename the export then this function would be unresolvable by name -// from anything linking against `libc.so`. -.globl cabi_realloc -.export_name cabi_realloc, "cabi_realloc" -.type cabi_realloc,@function -cabi_realloc: - .functype cabi_realloc (i32, i32, i32, i32) -> (i32) - - // Prior to bytecodealliance/wasmtime#13949 wasmtime's handling of - // `cabi_realloc` and context slots was a bit buggy. That means that prior - // to that PR the slots aren't guaranteed to be 0 and the value present will - // be used by some future task. This export is going to clobber these - // slots, so while that Wasmtime change is percolating this preserves the - // slots around the invocation of `cabi_realloc`. Once #13949 percolates and - // ships then this workaround code, and the restore down below, can be - // deleted. - .local i32, i32 - call __wasm_get_stack_pointer - local.set 4 -#ifdef __wasi_cooperative_threads__ - call wasip3_context_get_1 - local.set 5 -#endif - - // First up, configure the stack pointer. Get the base of the stack from the - // data symbol defined below, add the stack size, and that's what we're - // starting with. - i32.const cabi_realloc_stack@MBREL - global.get __memory_base - i32.add - i32.const REALLOC_STACK_SIZE - i32.add - call __wasm_set_stack_pointer - - // Next up configure TLS. This is only required in coop-threads builds. -#ifdef __wasi_cooperative_threads__ - global.get __init_tls_base - call __wasilibc_set_task_tls -#endif - - // And finally forward to the actual implementation of `cabi_realloc` - // defined in `wasip3.c` - local.get 0 - local.get 1 - local.get 2 - local.get 3 - call __cabi_realloc_impl - - local.get 4 - call __wasm_set_stack_pointer -#ifdef __wasi_cooperative_threads__ - local.get 5 - call wasip3_context_set_1 -#endif - - end_function - -// Define a data symbol which is the stack that `cabi_realloc` runs on. -// Note that this is aligned to 16-bytes, the expected stack alignment. -.type cabi_realloc_stack,@object -.section .bss.cabi_realloc_stack,"",@ -.p2align 4, 0x0 -cabi_realloc_stack: - .skip REALLOC_STACK_SIZE - .size cabi_realloc_stack, REALLOC_STACK_SIZE - -#endif // __wasm_libcall_thread_context__ diff --git a/libc-bottom-half/sources/__wasm_init_task.S b/libc-bottom-half/sources/__wasm_init_task.S deleted file mode 100644 index 531d27018..000000000 --- a/libc-bottom-half/sources/__wasm_init_task.S +++ /dev/null @@ -1,60 +0,0 @@ -#include - -.text - -.globaltype __init_stack_pointer, i32, immutable -.functype __wasm_set_stack_pointer (i32) -> () -.globl __wasm_set_stack_pointer - -#ifdef __wasi_cooperative_threads__ -#ifdef __PIC__ -.globaltype __init_tls_base, i32 -#else -.globaltype __init_tls_base, i32, immutable -#endif -.functype __wasilibc_set_task_tls (i32) -> () -#endif - - -// Entrypoint for initializing both async and sync tasks in the component model. -// -// Calls to this function are injected by `wit-component` when a component is -// created from a core module. Specifically for lifted functions this -// initialization hook is invoked first before the user-specified lifted -// function. The purpose of this function is to configure this task's stack/TLS -// setup and its `context.{get,set}` slots. -// -// For both sync and async tasks once they're entered in this component it's -// guaranteed that no other task will start before they have returned. That -// means that it's safe to use the same stack for all tasks. This does mean that -// the same TLS values are used across all tasks, which seem reasonable-enough -// for now. -// -// Note that configuring TLS is only required in coop threads builds. When -// threads are disabled TLS is handled by `wit-component`'s synthesis of -// functions. With coop threads though delegation goes into C in wasi-libc -// itself to do a bit of detection logic. -.globl __wasm_init_task -.export_name __wasm_init_task, __wasm_init_task -.type __wasm_init_task,@function -__wasm_init_task: - .functype __wasm_init_task () -> () - - global.get __init_stack_pointer - call __wasm_set_stack_pointer - -#ifdef __wasi_cooperative_threads__ - global.get __init_tls_base - call __wasilibc_set_task_tls -#endif - end_function - -// This is just a small wrapper around `__wasm_init_task`. Work needs to be done -// in `wit-component` to make this optional. -.globl __wasm_init_async_task -.type __wasm_init_async_task,@function -.export_name __wasm_init_async_task, __wasm_init_async_task -__wasm_init_async_task: - .functype __wasm_init_async_task () -> () - call __wasm_init_task - end_function diff --git a/libc-bottom-half/sources/__wasm_task_hook.S b/libc-bottom-half/sources/__wasm_task_hook.S new file mode 100644 index 000000000..ed4469993 --- /dev/null +++ b/libc-bottom-half/sources/__wasm_task_hook.S @@ -0,0 +1,96 @@ +#include + +// The size, in bytes, of the stack statically allocated for `__wasilibc_task_hook`. +// This doesn't need to be too big as it's just running `malloc` as defined +// in wasi-libc. The current number is determined by: +// +// * Start with 16 bytes. +// * Double until tests don't crash in a debug build. +// * Double again. +// +// The goal is to be pretty modest since this is part of every module. +#define HOOK_STACK_SIZE 512 + +.text + +.globl __wasm_set_stack_pointer +.functype __wasm_set_stack_pointer (i32) -> () +.globl __wasm_get_stack_pointer +.functype __wasm_get_stack_pointer () -> (i32) +.functype __wasilibc_task_hook (i32, i32, i32, i32, i32) -> (i32) + +.globaltype __init_stack_pointer, i32, immutable +.globaltype __memory_base, i32, immutable + +#ifdef __wasi_cooperative_threads__ +#ifdef __PIC__ +.globaltype __init_tls_base, i32 +#else +.globaltype __init_tls_base, i32, immutable +#endif +#endif + +// Hook point for component model task lifecycle events which is invoked by +// `wit-component`'s componentization process. +// +// Calls to this function are injected by `wit-component` when a component is +// created from a core module. This is invoked with single `i32` parameter +// indicating what's being hooked. Hooks include situations such as a +// synchronous task starting, an asynchronous task blocking, cabi_realloc +// returning, etc. The goal of this hook is to manage the `context.{get,set}` +// slots for this task, notably the stack pointer and TLS base pointer. +// +// When this function is invoked the context slots may both be 0, indicating +// that there is not a stack, hence the reason for this being an assembly +// function. This function quickly sets up a small temporary stack, allocated +// below as `hook_stack`, and then delegates to the `__wasilibc_task_hook` +// function which finishes the task started here. The return value of +// `__wasilibc_task_hook` becomes the stack pointer after this function returns. +.globl __wasm_task_hook +.export_name __wasm_task_hook, __wasm_task_hook +.type __wasm_task_hook,@function +__wasm_task_hook: + .functype __wasm_task_hook (i32) -> () + .local i32 + + // prep arguments to `__wasilibc_task_hook`, notably: + // + // * The hook being configured (this function's argument) + // * The TLS base for the main thread (to configure TLS) + // * The initial stack pointer for the main thread (if that'll get used) + // * The current stack pointer (if we're deallocating a stack) + // * The hook stack pointer (passed further down below) + local.get 0 +#ifdef __wasi_cooperative_threads__ + global.get __init_tls_base +#else + i32.const 0 +#endif + global.get __init_stack_pointer + call __wasm_get_stack_pointer + + // setup a temporary stack + i32.const hook_stack@MBREL + global.get __memory_base + i32.add + i32.const HOOK_STACK_SIZE + i32.add + local.tee 1 + call __wasm_set_stack_pointer + + // figure out what our stack should be after this hook invocation and + // configure it. + local.get 1 + call __wasilibc_task_hook + call __wasm_set_stack_pointer + + end_function + +// Define a data symbol which is the stack that `__wasilibc_task_hook` runs on. +// Note that this is aligned to 16-bytes, the expected stack alignment. +.type hook_stack,@object +.section .bss.hook_stack,"",@ +.p2align 4, 0x0 +hook_stack: + .skip HOOK_STACK_SIZE + .size hook_stack, HOOK_STACK_SIZE diff --git a/libc-bottom-half/sources/cabi_realloc_augment.h b/libc-bottom-half/sources/cabi_realloc_augment.h deleted file mode 100644 index 70ad826b9..000000000 --- a/libc-bottom-half/sources/cabi_realloc_augment.h +++ /dev/null @@ -1,21 +0,0 @@ -// This is a "surgical" augmentation for the generated `wasip{2,3}.c` files in -// wasi-libc which changes the behavior of the generated `cabi_realloc`. Ideally -// `wit-bindgen` would have a mode that doesn't generate `cabi_realloc`, but that -// doesn't exist right now so we're left to augment it. -// -// Specifically the change made here is that when -// `__wasm_libcall_thread_context__` is enabled, such as on wasip3 targets, the -// actual `cabi_realloc` entrypoint lives in an extern assembly file and the -// `cabi_realloc` symbol in the `wasip3.c` file is renamed to something else. -// This handles how `cabi_realloc` starts with context slots 0'd out and with -// a libcall thread context that means the stack needs to be configured. -// -// To make bindings generation a bit easier this file is `#include`'d in a a -// single location hence the trailing `__attribute__` at the end which is -// applicable when `__wasm_libcall_thread_context__` is disabled. - -#ifdef __wasm_libcall_thread_context__ -#define cabi_realloc __cabi_realloc_impl -#else -__attribute__((__weak__, __export_name__("cabi_realloc"))) -#endif diff --git a/libc-bottom-half/sources/wasip2.c b/libc-bottom-half/sources/wasip2.c index e59bd440c..caf7f9399 100644 --- a/libc-bottom-half/sources/wasip2.c +++ b/libc-bottom-half/sources/wasip2.c @@ -422,7 +422,7 @@ extern void __wasm_import_random_insecure_seed_insecure_seed(uint8_t *); // Canonical ABI intrinsics -#include "cabi_realloc_augment.h" +__attribute__((__weak__, __export_name__("cabi_realloc"))) void *cabi_realloc(void *ptr, size_t old_size, size_t align, size_t new_size) { (void) old_size; if (new_size == 0) return (void*) align; diff --git a/libc-bottom-half/sources/wasip3.c b/libc-bottom-half/sources/wasip3.c index 11f4c9d4d..a73a140c1 100644 --- a/libc-bottom-half/sources/wasip3.c +++ b/libc-bottom-half/sources/wasip3.c @@ -306,7 +306,7 @@ extern void __wasm_import_random_insecure_seed_get_insecure_seed(uint8_t *); // Canonical ABI intrinsics -#include "cabi_realloc_augment.h" +__attribute__((__weak__, __export_name__("cabi_realloc"))) void *cabi_realloc(void *ptr, size_t old_size, size_t align, size_t new_size) { (void) old_size; if (new_size == 0) return (void*) align; diff --git a/libc-bottom-half/sources/wasip3_tls.c b/libc-bottom-half/sources/wasip3_tls.c deleted file mode 100644 index 88fcb6ccf..000000000 --- a/libc-bottom-half/sources/wasip3_tls.c +++ /dev/null @@ -1,23 +0,0 @@ -#ifdef __wasm_libcall_thread_context__ - -#include -#include - -#ifdef __wasi_cooperative_threads__ - -void __wasilibc_set_task_tls(void *init_tls_base) { - const struct __wasilibc_program_tls_info *info = - __wasilibc_program_tls_info(); - - // With a single module in the component the TLS base is stored directly in - // context slot 1, so this module's own initial TLS is what belongs there. - // With more than one module the slot instead holds the array of per-module - // TLS base pointers, and the main thread's array was populated by each module - // as the component was instantiated. - wasip3_context_set_1(info == NULL ? init_tls_base - : (void *)info->main_thread_tls_base); -} - -#endif // __wasi_cooperative_threads__ - -#endif // __wasm_libcall_thread_context__ diff --git a/libc-top-half/musl/src/env/__init_tls.c b/libc-top-half/musl/src/env/__init_tls.c index e3d9e7623..612cb177e 100644 --- a/libc-top-half/musl/src/env/__init_tls.c +++ b/libc-top-half/musl/src/env/__init_tls.c @@ -15,6 +15,8 @@ #include "syscall.h" #include #include "lock.h" +#include +#include DECLARE_WEAK_LOCK(__thread_list_lock); @@ -285,3 +287,105 @@ static void static_init_tls(size_t *aux) weak_alias(static_init_tls, __init_tls); #endif + +#ifdef __wasm_libcall_thread_context__ + +// Whether or not the main program stack is in use by some task/thread in this +// program. +static bool init_stack_in_use = false; + +// Codes passed to `__wasilibc_task_hook` which are synthesized in the +// `wit-component` crate in wasm-tools. +#define SYNC_START 0 +#define SYNC_FINISH 1 +#define ASYNC_START 2 +#define ASYNC_RESUME 3 +#define ASYNC_BLOCK 4 +#define ASYNC_FINISH 5 +#define INITIALIZE_START 6 +#define INITIALIZE_FINISH 7 +#define RESOURCE_DTOR_START 8 +#define RESOURCE_DTOR_FINISH 9 +#define POST_RETURN_START 10 +#define POST_RETURN_FINISH 11 +#define REALLOC_START 12 +#define REALLOC_FINISH 13 + +// A hook executed by the `__wasm_task_hook` entrypoint defined in an external +// assembly file. For some more information about context see the documentation +// on the definition of `__wasm_task_hook`. +void *__wasilibc_task_hook(int hook, void *init_tls_base, void *init_stack_pointer, void *prev_stack, void *hook_stack) { + // For coop threads the TLS for this task needs to be configured. + // + // With a single module in the component the TLS base is stored directly in + // context slot 1, so this module's own initial TLS is what belongs there. + // With more than one module the slot instead holds the array of per-module + // TLS base pointers, and the main thread's array was populated by each module + // as the component was instantiated. +#ifdef __wasi_cooperative_threads__ + const struct __wasilibc_program_tls_info *info = + __wasilibc_program_tls_info(); + wasip3_context_set_1(info == NULL ? init_tls_base + : (void *)info->main_thread_tls_base); +#endif // __wasi_cooperative_threads__ + + // Otherwise with and without coop threads this is a hook which likely needs + // to configure the stack. By default the main stack is used which avoids the + // need to dynamically allocate a stack, but once the main stack is used + // then new stacks are dynamically allocated to get free'd later. + // + // Hooks happen for entering and leaving wasm and are injected by + // wit-component, so this handles both allocation and deallocation of the + // stack. Note that this function itself is executing on a small temporary + // stack for the duration of the call. + switch (hook) { + case SYNC_START: + case ASYNC_START: + case ASYNC_RESUME: + case INITIALIZE_START: + case RESOURCE_DTOR_START: + case POST_RETURN_START: + assert(!prev_stack); + if (!init_stack_in_use) { + init_stack_in_use = true; + return init_stack_pointer; + } + + struct stack_bounds bounds = get_stack_bounds(); + void *new_stack = malloc(bounds.size); + if (new_stack == NULL) + __builtin_trap(); + return new_stack + bounds.size; + + case SYNC_FINISH: + case ASYNC_BLOCK: + case ASYNC_FINISH: + case INITIALIZE_FINISH: + case RESOURCE_DTOR_FINISH: + case POST_RETURN_FINISH: + if (prev_stack == init_stack_pointer) { + init_stack_in_use = false; + } else { + free(prev_stack - get_stack_bounds().size); + } + return NULL; + + // For `realloc` we know that it'll return quickly, have bounded stack + // usage, and no be reentrant. Use the hook stack we're already executing + // on to avoid otherwise allocating a new stack. Without this, for example, + // returning a string from an import would always allocate a new stack + // which is a bit wasteful. + case REALLOC_START: + assert(!prev_stack); + return hook_stack; + case REALLOC_FINISH: + assert(prev_stack == hook_stack); + return NULL; + + default: + __builtin_trap(); + } +} + + +#endif // __wasm_libcall_thread_context__ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 72d753cb8..dd025fe63 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -52,9 +52,19 @@ if(TEST_WITH_V8) elseif(NOT ENGINE OR ENGINE STREQUAL "") # ====== Download Wasmtime as a test runner ================== - set(wasmtime_version dev) + # + # By default a pinned version of Wasmtime is used that's kept relatively + # up-to-date. Note though that `wasm32-wasip1-threads` requires an explicitly + # older version of Wasmtime as the latest releases no longer support + # wasi-threads. Additionally for wasip3's new ABI, e.g. + # `HAVE_WASM_LIBCALL_THREAD_CONTEXT`, a currently-unrelated version of + # Wasmtime is required which has various bugs fixed in it. + set(wasmtime_version v47.0.0) if (TARGET_TRIPLE MATCHES "-threads") - set(wasmtime_version "v44.0.0") + set(wasmtime_version v44.0.0) + endif() + if (HAVE_WASM_LIBCALL_THREAD_CONTEXT) + set(wasmtime_version dev) endif() ba_download( wasmtime @@ -156,6 +166,19 @@ function(add_test_executable executable_name src) else() clang_format_target(${executable_name}) target_link_libraries(${executable_name} PRIVATE c-static) + + if (NOT USE_WASM_COMPONENT_LD AND NOT WASI STREQUAL "p1") + target_link_options(${executable_name} PRIVATE -Wl,--skip-wit-component) + add_custom_command( + TARGET ${executable_name} + POST_BUILD + COMMAND + ${wasm_tools} component new + $ + -o ${executable_name} + ) + add_dependencies(${executable_name} wasm-tools) + endif() endif() foreach(flag IN LISTS arg_CFLAGS) target_compile_options(${executable_name} PRIVATE ${flag}) @@ -188,9 +211,11 @@ endfunction() # * `NETWORK` - this test uses the network and sockets. # * `PASS_REGULAR_EXPRESSION` - a regex that must match the test output to pass # * `SETJMP` - this test requires setjmp/longjmp +# * `FILE` - the wasm file to execute. Defaults to the output of +# `executable_name`. function(register_test test_name executable_name) set(options FS NETWORK SETJMP) - set(oneValueArgs CLIENT PASS_REGULAR_EXPRESSION ENGINE_ARG) + set(oneValueArgs CLIENT PASS_REGULAR_EXPRESSION ENGINE_ARG FILE) set(multiValueArgs ARGV ENV) cmake_parse_arguments(PARSE_ARGV 1 arg "${options}" "${oneValueArgs}" "${multiValueArgs}") @@ -228,12 +253,18 @@ function(register_test test_name executable_name) ) endif() + if (arg_FILE) + set(test_program ${arg_FILE}) + else() + set(test_program $) + endif() + add_test( NAME "${test_name}" COMMAND ${ENGINE} ${wasmtime_args} - $ ${arg_ARGV} + ${test_program} ${arg_ARGV} ) # Use CTest fixtures to create a the temporary directory before the test @@ -252,7 +283,7 @@ function(register_test test_name executable_name) set_tests_properties(${test_name} PROPERTIES TIMEOUT 10) - add_dependencies(${test_name} engine) + add_dependencies(${executable_name} engine) endfunction() # Helper function to add two versions of a test: a statically linked version @@ -654,19 +685,28 @@ endif() # ========= wasi-libc tests for non-main entrypoints ========================= -add_custom_command( - OUTPUT - ${CMAKE_CURRENT_BINARY_DIR}/entrypoint.c - ${CMAKE_CURRENT_BINARY_DIR}/entrypoint.h - ${CMAKE_CURRENT_BINARY_DIR}/entrypoint_component_type.o - COMMAND - ${wit_bindgen} c ${CMAKE_CURRENT_SOURCE_DIR}/entrypoint.wit - --out-dir ${CMAKE_CURRENT_BINARY_DIR} - DEPENDS wit-bindgen ${CMAKE_CURRENT_SOURCE_DIR}/entrypoint.wit -) -add_library(entrypoint_bindings STATIC EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_BINARY_DIR}/entrypoint.c - ${CMAKE_CURRENT_BINARY_DIR}/entrypoint_component_type.o) +function(add_wit_bindgen_library name wit world out_dir) + add_custom_command( + OUTPUT + ${out_dir}/${world}.c + ${out_dir}/${world}.h + ${out_dir}/${world}_component_type.o + COMMAND + ${wit_bindgen} c ${wit} --world ${world} --out-dir ${out_dir} + DEPENDS wit-bindgen ${wit} + ) + add_library(${name} STATIC EXCLUDE_FROM_ALL + ${out_dir}/${world}.c + ${out_dir}/${world}_component_type.o) + target_include_directories(${name} PUBLIC ${out_dir}) + target_compile_options(${name} PRIVATE -fPIC) +endfunction() + +add_wit_bindgen_library( + entrypoint_bindings + ${CMAKE_CURRENT_SOURCE_DIR}/entrypoint.wit + entrypoint + ${CMAKE_CURRENT_BINARY_DIR}) function(add_external_entrypoint_test test_file) cmake_path(REPLACE_EXTENSION test_file wasm OUTPUT_VARIABLE test_name) @@ -1194,6 +1234,82 @@ if (ENABLE_COOP_THREADS) # unsupported(cancellation): conformance/interfaces/pthread_testcancel/* endif() +# ========= Testing component compositions ============================= + +function(add_composition_test test_file) + if (WASI STREQUAL "p1") + return() + endif() + + set(bindgen_dir "${CMAKE_CURRENT_BINARY_DIR}/bindgen/${test_file}") + set(test_root "${CMAKE_CURRENT_SOURCE_DIR}/src/compositions") + set(wit_file "${test_root}/${test_file}.wit") + set(caller_src "${test_root}/${test_file}_caller.c") + set(callee_src "${test_root}/${test_file}_callee.c") + set(composed compose_${test_file}_composed) + set(composed_shared shared_compose_${test_file}_composed) + + add_wit_bindgen_library( + ${test_file}_caller_bindings + ${wit_file} + caller + ${bindgen_dir} + ) + add_wit_bindgen_library( + ${test_file}_callee_bindings + ${wit_file} + callee + ${bindgen_dir} + ) + + # Note that these use `-` instead of `_` because `wasm-tools compose` derives + # component names from file names and requires them to be kebab-case. + string(REPLACE "_" "-" kebab ${test_file}) + set(caller_out compose-${kebab}-caller.wasm) + set(callee_out compose-${kebab}-callee.wasm) + set(caller_out_shared shared-compose-${kebab}-caller.wasm) + set(callee_out_shared shared-compose-${kebab}-callee.wasm) + + add_test_executable(${caller_out} ${caller_src} + WHOLE_ARCHIVE ${test_file}_caller_bindings) + add_test_executable(${callee_out} ${callee_src} + WHOLE_ARCHIVE ${test_file}_callee_bindings) + compose_and_register_test(${composed} ${caller_out} ${callee_out}) + + if (BUILD_SHARED) + add_test_executable(${caller_out_shared} ${caller_src} + WHOLE_ARCHIVE ${test_file}_caller_bindings SHARED) + add_test_executable(${callee_out_shared} ${callee_src} + WHOLE_ARCHIVE ${test_file}_callee_bindings SHARED) + compose_and_register_test(${composed_shared} + ${caller_out_shared} ${callee_out_shared}) + endif() +endfunction() + +# Composes `caller` with `callee` into `${name}.wasm` and registers the result +# as a test named `${name}`. +# +# Note that `add_custom_command` only declares how to produce a file, so a +# custom target is needed both to get the composition built as part of `all` +# and to give `register_test` something to hang `add_dependencies` off of. +function(compose_and_register_test name caller callee) + set(output "${CMAKE_CURRENT_BINARY_DIR}/${name}.wasm") + add_custom_command( + OUTPUT ${output} + COMMAND ${wasm_tools} compose $ + -d $ -o ${output} --quiet + DEPENDS ${caller} ${callee} wasm-tools) + add_custom_target(${name} ALL DEPENDS ${output}) + register_test(${name} ${name} FILE ${output}) +endfunction() + +add_composition_test(simple) +add_composition_test(simple_resource) +add_composition_test(simple_string) +add_composition_test(barge_in) + +# ========= Testing Python ============================= + # If enabled add a copy of Python which is built against `wasi-libc` and run # its tests. if (PYTHON_TESTS) diff --git a/test/src/compositions/barge_in.wit b/test/src/compositions/barge_in.wit new file mode 100644 index 000000000..37c0caefc --- /dev/null +++ b/test/src/compositions/barge_in.wit @@ -0,0 +1,16 @@ +package test:simple; + +interface i { + blocking: async func(); + barge-in: func(); + barge-in-with-post-return: func() -> string; + t-status: func() -> u32; +} + +world caller { + import i; +} + +world callee { + export i; +} diff --git a/test/src/compositions/barge_in_callee.c b/test/src/compositions/barge_in_callee.c new file mode 100644 index 000000000..f31fca1f9 --- /dev/null +++ b/test/src/compositions/barge_in_callee.c @@ -0,0 +1,50 @@ +#include "test.h" +#include + +#define N 200 +#define TEST(c) \ + do { \ + if (!(c)) \ + t_error("%s failed\n", #c); \ + } while (0) + +callee_callback_code_t exports_test_simple_i_blocking(void) { + int my_stack_array[N]; + + for (int i = 0; i < N; i++) + my_stack_array[i] = i; + + callee_thread_yield(); + + for (int i = 0; i < N; i++) + TEST(my_stack_array[i] == i); + + exports_test_simple_i_blocking_return(); + return CALLEE_CALLBACK_CODE_EXIT; +} + +callee_callback_code_t +exports_test_simple_i_blocking_callback(callee_event_t *event) { + (void)event; + __builtin_trap(); +} + +void exports_test_simple_i_barge_in() { + int my_stack_array[N]; + + for (int i = 0; i < N; i++) + my_stack_array[i] = i + N; + + for (int i = 0; i < N; i++) + TEST(my_stack_array[i] == i + N); +} + +void exports_test_simple_i_barge_in_with_post_return(callee_string_t *ret) { + ret->ptr = NULL; + ret->len = 0; + + // smash the stack a bit here too. + exports_test_simple_i_barge_in(); +} + +uint32_t exports_test_simple_i_t_status() { return t_status; } diff --git a/test/src/compositions/barge_in_caller.c b/test/src/compositions/barge_in_caller.c new file mode 100644 index 000000000..d04f13eb8 --- /dev/null +++ b/test/src/compositions/barge_in_caller.c @@ -0,0 +1,28 @@ +#include "test.h" +#include + +#define TEST(c) \ + do { \ + if (!(c)) \ + t_error("%s failed\n", #c); \ + } while (0) + +int main() { + caller_subtask_status_t status = test_simple_i_blocking(); + TEST(CALLER_SUBTASK_STATE(status) == CALLER_SUBTASK_STARTED); + caller_subtask_t subtask = CALLER_SUBTASK_HANDLE(status); + + test_simple_i_barge_in(); + + // For wasip2 this `main` function is a synchronous entrypoint so we can't + // cancel the task which would block, but for wasip3 and beyond it's ok to + // block so it's ok to cancel. +#ifdef __wasip2__ + (void)subtask; +#else + status = caller_subtask_cancel(subtask); + TEST(CALLER_SUBTASK_STATE(status) == CALLER_SUBTASK_RETURNED); +#endif + + return t_status + test_simple_i_t_status(); +} diff --git a/test/src/compositions/simple.wit b/test/src/compositions/simple.wit new file mode 100644 index 000000000..7dd4e0e9c --- /dev/null +++ b/test/src/compositions/simple.wit @@ -0,0 +1,13 @@ +package test:simple; + +interface i { + hello: func() -> u32; +} + +world caller { + import i; +} + +world callee { + export i; +} diff --git a/test/src/compositions/simple_callee.c b/test/src/compositions/simple_callee.c new file mode 100644 index 000000000..03fefed52 --- /dev/null +++ b/test/src/compositions/simple_callee.c @@ -0,0 +1,3 @@ +#include + +uint32_t exports_test_simple_i_hello() { return 7; } diff --git a/test/src/compositions/simple_caller.c b/test/src/compositions/simple_caller.c new file mode 100644 index 000000000..7024e5ae1 --- /dev/null +++ b/test/src/compositions/simple_caller.c @@ -0,0 +1,13 @@ +#include "test.h" +#include + +#define TEST(c) \ + do { \ + if (!(c)) \ + t_error("%s failed\n", #c); \ + } while (0) + +int main() { + TEST(test_simple_i_hello() == 7); + return t_status; +} diff --git a/test/src/compositions/simple_resource.wit b/test/src/compositions/simple_resource.wit new file mode 100644 index 000000000..8ac7a3adc --- /dev/null +++ b/test/src/compositions/simple_resource.wit @@ -0,0 +1,19 @@ +package test:simple; + +interface i { + resource r { + constructor(x: u32); + get: func() -> u32; + set: func(x: u32); + } + + t-status: func() -> u32; +} + +world caller { + import i; +} + +world callee { + export i; +} diff --git a/test/src/compositions/simple_resource_callee.c b/test/src/compositions/simple_resource_callee.c new file mode 100644 index 000000000..a860a0d5f --- /dev/null +++ b/test/src/compositions/simple_resource_callee.c @@ -0,0 +1,38 @@ +#include "test.h" +#include +#include + +#define TEST(c) \ + do { \ + if (!(c)) \ + t_error("%s failed\n", #c); \ + } while (0) + +struct exports_test_simple_i_r_t { + uint32_t val; +}; + +void exports_test_simple_i_r_destructor(exports_test_simple_i_r_t *loc) { + TEST(loc); + free(loc); +} + +exports_test_simple_i_own_r_t exports_test_simple_i_constructor_r(uint32_t x) { + exports_test_simple_i_r_t *loc = malloc(sizeof(exports_test_simple_i_r_t)); + TEST(loc); + loc->val = x; + return exports_test_simple_i_r_new(loc); +} + +uint32_t exports_test_simple_i_method_r_get(exports_test_simple_i_r_t *loc) { + TEST(loc); + return loc->val; +} + +void exports_test_simple_i_method_r_set(exports_test_simple_i_r_t *loc, + uint32_t x) { + TEST(loc); + loc->val = x; +} + +uint32_t exports_test_simple_i_t_status() { return t_status; } diff --git a/test/src/compositions/simple_resource_caller.c b/test/src/compositions/simple_resource_caller.c new file mode 100644 index 000000000..a4e207fad --- /dev/null +++ b/test/src/compositions/simple_resource_caller.c @@ -0,0 +1,18 @@ +#include "test.h" +#include + +#define TEST(c) \ + do { \ + if (!(c)) \ + t_error("%s failed\n", #c); \ + } while (0) + +int main() { + test_simple_i_own_r_t r = test_simple_i_constructor_r(10); + test_simple_i_borrow_r_t r2 = test_simple_i_borrow_r(r); + TEST(test_simple_i_method_r_get(r2) == 10); + test_simple_i_method_r_set(r2, 12); + TEST(test_simple_i_method_r_get(r2) == 12); + test_simple_i_r_drop_own(r); + return t_status + test_simple_i_t_status(); +} diff --git a/test/src/compositions/simple_string.wit b/test/src/compositions/simple_string.wit new file mode 100644 index 000000000..cf1782c7a --- /dev/null +++ b/test/src/compositions/simple_string.wit @@ -0,0 +1,14 @@ +package test:simple; + +interface i { + hello: func(x: string) -> string; + t-status: func() -> u32; +} + +world caller { + import i; +} + +world callee { + export i; +} diff --git a/test/src/compositions/simple_string_callee.c b/test/src/compositions/simple_string_callee.c new file mode 100644 index 000000000..e9e865bcb --- /dev/null +++ b/test/src/compositions/simple_string_callee.c @@ -0,0 +1,17 @@ +#include "test.h" +#include +#include + +#define TEST(c) \ + do { \ + if (!(c)) \ + t_error("%s failed\n", #c); \ + } while (0) + +void exports_test_simple_i_hello(callee_string_t *x, callee_string_t *ret) { + TEST(x->len == 5); + TEST(memcmp(x->ptr, "hello", 5) == 0); + *ret = *x; +} + +uint32_t exports_test_simple_i_t_status() { return t_status; } diff --git a/test/src/compositions/simple_string_caller.c b/test/src/compositions/simple_string_caller.c new file mode 100644 index 000000000..b89420da8 --- /dev/null +++ b/test/src/compositions/simple_string_caller.c @@ -0,0 +1,19 @@ +#include "test.h" +#include +#include + +#define TEST(c) \ + do { \ + if (!(c)) \ + t_error("%s failed\n", #c); \ + } while (0) + +int main() { + caller_string_t arg, ret; + arg.ptr = (uint8_t *)"hello"; + arg.len = 5; + test_simple_i_hello(&arg, &ret); + TEST(ret.len == 5); + TEST(memcmp(ret.ptr, "hello", 5) == 0); + return t_status + test_simple_i_t_status(); +}