From 3919e435c408d1103fd32027d35743e3910e40b0 Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Tue, 11 Aug 2020 13:08:33 -0700 Subject: [PATCH] Clean up WASI 'Wasm C API' API --- lib/c-api/src/wasm_c_api/wasi/mod.rs | 36 +++--- lib/c-api/tests/.gitignore | 5 +- lib/c-api/tests/CMakeLists.txt | 1 + lib/c-api/tests/wasm-c-api-wasi.c | 160 +++++++++++++++++++++++++++ lib/c-api/wasmer_wasm.h | 136 +++++++++++++++++++++++ 5 files changed, 314 insertions(+), 24 deletions(-) create mode 100644 lib/c-api/tests/wasm-c-api-wasi.c create mode 100644 lib/c-api/wasmer_wasm.h diff --git a/lib/c-api/src/wasm_c_api/wasi/mod.rs b/lib/c-api/src/wasm_c_api/wasi/mod.rs index a90425daa..667153961 100644 --- a/lib/c-api/src/wasm_c_api/wasi/mod.rs +++ b/lib/c-api/src/wasm_c_api/wasi/mod.rs @@ -63,7 +63,8 @@ pub unsafe extern "C" fn wasi_state_builder_arg( state_builder.arg(arg_bytes); } -// must be transparent +// NOTE: don't modify this type without updating all users of it. We rely on +// this struct being `repr(transparent)` with `Box` in the API. #[repr(transparent)] pub struct wasi_file_handle_t { inner: Box, @@ -100,7 +101,6 @@ pub unsafe extern "C" fn wasi_output_capturing_file_read( } } -// TODO: figure out ownership here /// Override the Stdout that the WASI program will see. /// /// This function takes ownership of the `wasi_file_handle_t` passed in. @@ -127,15 +127,11 @@ pub unsafe extern "C" fn wasi_state_builder_set_stderr( state_builder.stderr(stderr.inner); } +// NOTE: don't modify this type without updating all users of it. We rely on +// this struct being `repr(transparent)` with `WasiState` in the API. #[allow(non_camel_case_types)] #[repr(transparent)] pub struct wasi_state_t { - inner: Box, -} - -#[allow(non_camel_case_types)] -#[repr(transparent)] -pub struct wasi_state_borrowed_t { inner: WasiState, } @@ -144,26 +140,26 @@ pub struct wasi_state_borrowed_t { pub extern "C" fn wasi_state_builder_build( mut state_builder: Box, ) -> Option> { - let inner = Box::new(c_try!(state_builder.build())); + let inner = c_try!(state_builder.build()); Some(Box::new(wasi_state_t { inner })) } #[allow(non_camel_case_types)] #[repr(C)] pub struct wasi_env_t { - inner: Box, + inner: WasiEnv, } /// Takes ownership over the `wasi_state_t`. #[no_mangle] pub extern "C" fn wasi_env_new(state: Box) -> Box { Box::new(wasi_env_t { - inner: Box::new(WasiEnv::new(*state.inner)), + inner: WasiEnv::new(state.inner), }) } #[no_mangle] -pub extern "C" fn wasi_env_delete(_state: Option>) {} +pub extern "C" fn wasi_env_delete(_state: Option>) {} #[no_mangle] pub extern "C" fn wasi_env_set_memory(env: &mut wasi_env_t, memory: &wasm_memory_t) { @@ -171,17 +167,19 @@ pub extern "C" fn wasi_env_set_memory(env: &mut wasi_env_t, memory: &wasm_memory } #[no_mangle] -pub extern "C" fn wasi_env_borrow_state(env: &wasi_env_t) -> &wasi_state_borrowed_t { +pub extern "C" fn wasi_env_borrow_state(env: &wasi_env_t) -> &wasi_state_t { let state: &WasiState = &*env.inner.state(); + // This is correct because `wasi_state_t` is `repr(transparent)` to `WasiState` unsafe { mem::transmute(state) } } /// returns a non-owning reference to stdout #[no_mangle] pub extern "C" fn wasi_state_get_stdout( - state: &wasi_state_borrowed_t, + state: &wasi_state_t, ) -> Option<&Option> { let inner: &Option> = c_try!(state.inner.fs.stdout()); + // This is correct because `wasi_file_handle_t` is `repr(transparent)` to `Box` let temp = unsafe { mem::transmute::<_, &'static Option>(inner) }; Some(temp) } @@ -241,7 +239,7 @@ pub unsafe extern "C" fn wasi_get_imports( //let version = c_try!(WasiVersion::try_from(version)); let version = WasiVersion::try_from(version).ok()?; - let import_object = generate_import_object_from_env(store, (&*wasi_env.inner).clone(), version); + let import_object = generate_import_object_from_env(store, wasi_env.inner.clone(), version); // TODO: this is very inefficient due to all the allocation required let mut extern_vec = vec![]; @@ -257,11 +255,3 @@ pub unsafe extern "C" fn wasi_get_imports( Some(extern_vec.into_boxed_slice()) } - -// get WASI import object - -// TASKS TODO: -// - [x] clean up and simplify caputure code -// - [ ] generate header file for this new WASI API -// - [ ] get import objects working -// - [ ] finish C example diff --git a/lib/c-api/tests/.gitignore b/lib/c-api/tests/.gitignore index 2b5e09c0f..3d28fe7a6 100644 --- a/lib/c-api/tests/.gitignore +++ b/lib/c-api/tests/.gitignore @@ -31,4 +31,7 @@ test-wasi-import-object test-emscripten-import-object # ignore wasm-c-api binaries -wasm-c-api-* \ No newline at end of file +wasm-c-api-* + +# Unignore files ending with `.c` (i.e. `wasm-c-api-wasi.c`) +!*.c \ No newline at end of file diff --git a/lib/c-api/tests/CMakeLists.txt b/lib/c-api/tests/CMakeLists.txt index 8b831624e..c40d8ea06 100644 --- a/lib/c-api/tests/CMakeLists.txt +++ b/lib/c-api/tests/CMakeLists.txt @@ -44,6 +44,7 @@ if (DEFINED EMSCRIPTEN_TESTS) endif() include_directories(wasm-c-api/include) +include_directories(..) find_library( diff --git a/lib/c-api/tests/wasm-c-api-wasi.c b/lib/c-api/tests/wasm-c-api-wasi.c new file mode 100644 index 000000000..f036dc447 --- /dev/null +++ b/lib/c-api/tests/wasm-c-api-wasi.c @@ -0,0 +1,160 @@ +#include +#include +#include +#include + +//#include "wasm.h" +#include "wasmer_wasm.h" + +#define own + +// Use the last_error API to retrieve error messages +void print_wasmer_error() +{ + int error_len = wasmer_last_error_length(); + printf("Error len: `%d`\n", error_len); + char *error_str = malloc(error_len); + wasmer_last_error_message(error_str, error_len); + printf("Error str: `%s`\n", error_str); +} + + +int main(int argc, const char* argv[]) { + // Initialize. + printf("Initializing...\n"); + wasm_engine_t* engine = wasm_engine_new(); + wasm_store_t* store = wasm_store_new(engine); + + // Load binary. + printf("Loading binary...\n"); + FILE* file = fopen("assets/qjs.wasm", "r"); + if (!file) { + printf("> Error loading module!\n"); + return 1; + } + fseek(file, 0L, SEEK_END); + size_t file_size = ftell(file); + fseek(file, 0L, SEEK_SET); + wasm_byte_vec_t binary; + wasm_byte_vec_new_uninitialized(&binary, file_size); + if (fread(binary.data, file_size, 1, file) != 1) { + printf("> Error loading module!\n"); + return 1; + } + fclose(file); + + // Compile. + printf("Compiling module...\n"); + own wasm_module_t* module = wasm_module_new(store, &binary); + if (!module) { + printf("> Error compiling module!\n"); + return 1; + } + + wasm_byte_vec_delete(&binary); + + printf("Setting up WASI...\n"); + wasi_file_handle_t* stdout_capturer = wasi_output_capturing_file_new(); + + wasi_state_builder_t* wsb = wasi_state_builder_new("example_program"); + // TODO: error checking + const char* js_string = "function greet(name) { return JSON.stringify('Hello, ' + name); }; print(greet('World'));"; + wasi_state_builder_arg(wsb, "--eval"); + wasi_state_builder_arg(wsb, js_string); + wasi_state_builder_set_stdout(wsb, stdout_capturer); + + wasi_state_t* wasi_state = wasi_state_builder_build(wsb); + if (!wasi_state) { + printf("> Error building WASI state!\n"); + return 1; + } + wasi_env_t* wasi_env = wasi_env_new(wasi_state); + if (!wasi_env) { + printf("> Error building WASI env!\n"); + print_wasmer_error(); + return 1; + } + wasi_version_t version = wasi_get_wasi_version(module); + + // Instantiate. + printf("Instantiating module...\n"); + const wasm_extern_t* const* imports = wasi_get_imports(store, module, wasi_env, version); + if (!imports) { + printf("> Error getting WASI imports!\n"); + print_wasmer_error(); + return 1; + } + own wasm_instance_t* instance = + wasm_instance_new(store, module, imports, NULL); + if (!instance) { + printf("> Error instantiating module!\n"); + print_wasmer_error(); + return 1; + } + + + // Extract export. + printf("Extracting export...\n"); + own wasm_extern_vec_t exports; + wasm_instance_exports(instance, &exports); + if (exports.size == 0) { + printf("> Error accessing exports!\n"); + return 1; + } + fprintf(stderr, "found %zu exports\n", exports.size); + + printf("Getting memory...\n"); + const wasm_memory_t* memory = wasm_extern_as_memory(exports.data[0]); + if (! memory) { + printf("Could not get memory!\n"); + return 1; + } + wasi_env_set_memory(wasi_env, memory); + const wasm_func_t* run_func = wasm_extern_as_func(exports.data[1]); + if (run_func == NULL) { + printf("> Error accessing export!\n"); + return 1; + } + + wasm_module_delete(module); + wasm_instance_delete(instance); + + // Call. + printf("Calling export...\n"); + printf("Evaluating \"%s\"\n", js_string); + if (wasm_func_call(run_func, NULL, NULL)) { + printf("> Error calling function!\n"); + return 1; + } + + const int BUF_SIZE = 128; + char buffer[BUF_SIZE] = { }; + wasi_state_t* wasi_state_ref = wasi_env_borrow_state(wasi_env); + wasi_file_handle_t* stdout_handle = wasi_state_get_stdout(wasi_state_ref); + if (!stdout_handle) { + printf("> Error getting stdout!\n"); + print_wasmer_error(); + return 1; + } + size_t result = BUF_SIZE; + for (size_t i = 0; + // TODO: this code is too clever, make the control flow more obvious here + result == BUF_SIZE && + (result = wasi_output_capturing_file_read(stdout_handle, buffer, BUF_SIZE, i * BUF_SIZE)); + ++i) { + printf("%.*s", BUF_SIZE, buffer); + } + printf("\n"); + + wasm_extern_vec_delete(&exports); + + // Shut down. + printf("Shutting down...\n"); + wasi_env_delete(wasi_env); + wasm_store_delete(store); + wasm_engine_delete(engine); + + // All done. + printf("Done.\n"); + return 0; +} diff --git a/lib/c-api/wasmer_wasm.h b/lib/c-api/wasmer_wasm.h new file mode 100644 index 000000000..ad7e4110b --- /dev/null +++ b/lib/c-api/wasmer_wasm.h @@ -0,0 +1,136 @@ +// This header file is for Wasmer APIs intended to be used with the standard Wasm C API. + +#ifndef WASMER_WASM_H +#define WASMER_WASM_H + +#include +#include "wasm.h" + +#define own + +// In order to use WASI, we need a `wasi_env_t`, but first we need a `wasi_state_t`. +// +// We get a `wasi_state_t` by building it with the `wasi_state_builder_t`. +// Once we have a `wasi_state_t`, we can use `wasi_env_new` to create a `wasi_env_t`. +// +// Once we have a `wasi_env_t` we must: +// - set it up with `wasi_env_set_memory` to expose a memory to the WASI host functions +// - call `wasi_get_imports` to get an array of imports needed to instantiate the Wasm module. + +// Used to build a `wasi_state_t`. +typedef struct wasi_state_builder_t wasi_state_builder_t; +// An opaque file handle to a WASI file. +typedef struct wasi_file_handle_t wasi_file_handle_t; +// The core WASI data structure, used to create a `wasi_env_t`. +typedef struct wasi_state_t wasi_state_t; +// This type is passed to the WASI host functions and owns a `wasi_state_t`. +typedef struct wasi_env_t wasi_env_t; + +// The version of WASI to use. +typedef uint32_t wasi_version_t; + +const uint32_t WASI_VERSION_LATEST = 0; +const uint32_t WASI_VERSION_SNAPSHOT0 = 1; +const uint32_t WASI_VERSION_SNAPSHOT1 = 2; +const uint32_t WASI_VERSION_INVALID = ~0; + + +// Create a `wasi_state_builder_t`. +// +// Takes as an argument the name of the Wasm program to execute (will show up +// as argv[0] to the Wasm program). +own wasi_state_builder_t* wasi_state_builder_new(const char* program_name); + +// Add an argument to be passed to the Wasi program. +void wasi_state_builder_arg(wasi_state_builder_t*, const char* arg); + +// Add an environment variable to be passed to the Wasi program. +void wasi_state_builder_env(wasi_state_builder_t*, const char* key, const char* value); + +// Override `sdtout` with the given `wasi_file_handle_t`. +void wasi_state_builder_set_stdout(wasi_state_builder_t*, wasi_file_handle_t*); + +// Consume the `wasi_state_builder_t` and get a `wasi_state_t`. +own wasi_state_t* wasi_state_builder_build(own wasi_state_builder_t*); + +// Create a `wasi_env_t`. +own wasi_env_t* wasi_env_new(own wasi_state_t*); + +// Delete the `wasi_env_t`, used to clean up all the resources used by WASI. +void wasi_env_delete(own wasi_env_t*); + +// Get an array of imports that can be used to instantiate the given module. +own const wasm_extern_t* own const* wasi_get_imports(wasm_store_t*, wasm_module_t*, wasi_env_t*, wasi_version_t); + +// TODO: investigate removing this part of the API +// TODO: investigate removing the wasi_version stuff from the API +// Set the memory in the `wasi_env_t` so that the WASI host functions can access WASI's memory. +void wasi_env_set_memory(wasi_env_t*, const wasm_memory_t*); + +// Get temporary access to `wasi_state_t` owned by the given `wasi_env_t`. +wasi_state_t* wasi_env_borrow_state(const wasi_env_t*); + +// Get the version of WASI needed by the given Wasm module. +wasi_version_t wasi_get_wasi_version(wasm_module_t*); + +// TODO: consider using a circular buffer and making read a mutable operation to +// avoid wasted memory. +// Create a capturing WASI file. This file stores all data written to it. +own wasi_file_handle_t* wasi_output_capturing_file_new(); + +// Delete an owned `wasi_file_handle_t` +void wasi_output_capturing_file_delete(own wasi_file_handle_t*); + +// Read from a capturing file (created by `wasi_output_capturing_file_new`. +size_t wasi_output_capturing_file_read(wasi_file_handle_t* file, + char* buffer, + size_t buffer_len, + size_t start_offset); + +// Get temporary access to the `stdout` WASI file. +wasi_file_handle_t* wasi_state_get_stdout(wasi_state_t*); + +// TODO: figure out if we can do less duplication. +/** + * Gets the length in bytes of the last error if any. + * + * This can be used to dynamically allocate a buffer with the correct number of + * bytes needed to store a message. + * + * See `wasmer_last_error_message()` to get a full example. + */ +int wasmer_last_error_length(); + +/** + * Gets the last error message if any into the provided buffer + * `buffer` up to the given `length`. + * + * The `length` parameter must be large enough to store the last + * error message. Ideally, the value should come from + * `wasmer_last_error_length()`. + * + * The function returns the length of the string in bytes, `-1` if an + * error occurs. Potential errors are: + * + * * The buffer is a null pointer, + * * The buffer is too smal to hold the error message. + * + * Note: The error message always has a trailing null character. + * + * Example: + * + * ```c + * int error_length = wasmer_last_error_length(); + * + * if (error_length > 0) { + * char *error_message = malloc(error_length); + * wasmer_last_error_message(error_message, error_length); + * printf("Error message: `%s`\n", error_message); + * } else { + * printf("No error message\n"); + * } + * ``` + */ +int wasmer_last_error_message(char* buffer, int length); + +#endif /* WASMER_WASM_H */