diff --git a/crates/wiggle/.github/workflows/main.yml b/crates/wiggle/.github/workflows/main.yml new file mode 100644 index 0000000000..4d66e0c055 --- /dev/null +++ b/crates/wiggle/.github/workflows/main.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + +jobs: + rustfmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - name: Install Rust + run: | + rustup update stable + rustup default stable + rustup component add rustfmt + - name: Cargo fmt + run: cargo fmt --all -- --check + + build: + name: Build + runs-on: ${{ matrix.os }}-latest + strategy: + matrix: + os: [ubuntu, macOS, windows] + + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - name: Install Rust + shell: bash + run: | + rustup update stable + rustup default stable + - name: Build + run: cargo build --all --release -vv + + test: + name: Test + runs-on: ${{ matrix.os }}-latest + strategy: + matrix: + os: [ubuntu, macOS, windows] + + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - name: Install Rust + shell: bash + run: | + rustup update stable + rustup default stable + - name: Test + run: cargo test --all diff --git a/crates/wiggle/.gitignore b/crates/wiggle/.gitignore new file mode 100644 index 0000000000..d95f1f6a78 --- /dev/null +++ b/crates/wiggle/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock +proptest-regressions diff --git a/crates/wiggle/Cargo.toml b/crates/wiggle/Cargo.toml new file mode 100644 index 0000000000..1d65785e39 --- /dev/null +++ b/crates/wiggle/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "wiggle" +version = "0.1.0" +authors = ["Pat Hickey ", "Jakub Konka "] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +wiggle-generate = { path = "crates/generate" } +witx = "0.8.3" +syn = { version = "1.0", features = ["full"] } + +[dev-dependencies] +wiggle-runtime = { path = "crates/runtime" } +wiggle-test = { path = "crates/test" } +proptest = "0.9" + +[workspace] +members = [ + "crates/generate", + "crates/runtime", + "crates/test", +] +exclude = ["crates/WASI"] diff --git a/crates/wiggle/LICENSE b/crates/wiggle/LICENSE new file mode 100644 index 0000000000..c46ee5f199 --- /dev/null +++ b/crates/wiggle/LICENSE @@ -0,0 +1,221 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + + diff --git a/crates/wiggle/README.md b/crates/wiggle/README.md new file mode 100644 index 0000000000..5b6db02f81 --- /dev/null +++ b/crates/wiggle/README.md @@ -0,0 +1,6 @@ +# wiggle + +An experimental implementation of `bytecodealliance/wig` crate which +generates Rust bindings from `*.witx` that are meant to be more idiomatic +and hopefully allowing for easier polyfilling between different WASI +snapshot versions in the future. diff --git a/crates/wiggle/crates/generate/.gitignore b/crates/wiggle/crates/generate/.gitignore new file mode 100644 index 0000000000..eb5a316cbd --- /dev/null +++ b/crates/wiggle/crates/generate/.gitignore @@ -0,0 +1 @@ +target diff --git a/crates/wiggle/crates/generate/Cargo.toml b/crates/wiggle/crates/generate/Cargo.toml new file mode 100644 index 0000000000..39a5814f86 --- /dev/null +++ b/crates/wiggle/crates/generate/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wiggle-generate" +version = "0.1.0" +authors = ["Pat Hickey ", "Jakub Konka "] +edition = "2018" + +[lib] + +[dependencies] +wiggle-runtime = { path = "../runtime" } +witx = "0.8.3" +quote = "1.0" +proc-macro2 = "1.0" +heck = "0.3" +anyhow = "1" +syn = { version = "1.0", features = ["full"] } diff --git a/crates/wiggle/crates/generate/src/config.rs b/crates/wiggle/crates/generate/src/config.rs new file mode 100644 index 0000000000..0941e73431 --- /dev/null +++ b/crates/wiggle/crates/generate/src/config.rs @@ -0,0 +1,105 @@ +use std::path::PathBuf; + +use proc_macro2::Span; +use syn::{ + braced, bracketed, + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Error, Ident, LitStr, Result, Token, +}; + +#[derive(Debug, Clone)] +pub struct Config { + pub witx: WitxConf, + pub ctx: CtxConf, +} + +#[derive(Debug, Clone)] +pub enum ConfigField { + Witx(WitxConf), + Ctx(CtxConf), +} + +impl ConfigField { + pub fn parse_pair(ident: &str, value: ParseStream, err_loc: Span) -> Result { + match ident { + "witx" => Ok(ConfigField::Witx(value.parse()?)), + "ctx" => Ok(ConfigField::Ctx(value.parse()?)), + _ => Err(Error::new(err_loc, "expected `witx` or `ctx`")), + } + } +} + +impl Parse for ConfigField { + fn parse(input: ParseStream) -> Result { + let id: Ident = input.parse()?; + let _colon: Token![:] = input.parse()?; + Self::parse_pair(id.to_string().as_ref(), input, id.span()) + } +} + +impl Config { + pub fn build(fields: impl Iterator, err_loc: Span) -> Result { + let mut witx = None; + let mut ctx = None; + for f in fields { + match f { + ConfigField::Witx(c) => { + witx = Some(c); + } + ConfigField::Ctx(c) => { + ctx = Some(c); + } + } + } + Ok(Config { + witx: witx + .take() + .ok_or_else(|| Error::new(err_loc, "`witx` field required"))?, + ctx: ctx + .take() + .ok_or_else(|| Error::new(err_loc, "`ctx` field required"))?, + }) + } +} + +impl Parse for Config { + fn parse(input: ParseStream) -> Result { + let contents; + let _lbrace = braced!(contents in input); + let fields: Punctuated = + contents.parse_terminated(ConfigField::parse)?; + Ok(Config::build(fields.into_iter(), input.span())?) + } +} + +#[derive(Debug, Clone)] +pub struct WitxConf { + pub paths: Vec, +} + +impl Parse for WitxConf { + fn parse(input: ParseStream) -> Result { + let content; + let _ = bracketed!(content in input); + let path_lits: Punctuated = content.parse_terminated(Parse::parse)?; + let paths: Vec = path_lits + .iter() + .map(|lit| PathBuf::from(lit.value())) + .collect(); + Ok(WitxConf { paths }) + } +} + +#[derive(Debug, Clone)] +pub struct CtxConf { + pub name: Ident, +} + +impl Parse for CtxConf { + fn parse(input: ParseStream) -> Result { + Ok(CtxConf { + name: input.parse()?, + }) + } +} diff --git a/crates/wiggle/crates/generate/src/funcs.rs b/crates/wiggle/crates/generate/src/funcs.rs new file mode 100644 index 0000000000..6433f7712a --- /dev/null +++ b/crates/wiggle/crates/generate/src/funcs.rs @@ -0,0 +1,263 @@ +use proc_macro2::TokenStream; +use quote::quote; + +use crate::lifetimes::anon_lifetime; +use crate::names::Names; + +pub fn define_func(names: &Names, func: &witx::InterfaceFunc) -> TokenStream { + let funcname = func.name.as_str(); + + let ident = names.func(&func.name); + let ctx_type = names.ctx_type(); + let coretype = func.core_type(); + + let params = coretype.args.iter().map(|arg| { + let name = names.func_core_arg(arg); + let atom = names.atom_type(arg.repr()); + quote!(#name : #atom) + }); + + let abi_args = quote!( + ctx: &#ctx_type, memory: &dyn wiggle_runtime::GuestMemory, + #(#params),* + ); + let abi_ret = if let Some(ret) = &coretype.ret { + match ret.signifies { + witx::CoreParamSignifies::Value(atom) => names.atom_type(atom), + _ => unreachable!("ret should always be passed by value"), + } + } else if func.noreturn { + // Ideally we would return `quote!(!)` here, but, we'd have to change + // the error handling logic in all the marshalling code to never return, + // and instead provide some other way to bail to the context... + // noreturn func + unimplemented!("noreturn funcs not supported yet!") + } else { + quote!(()) + }; + + let err_type = coretype.ret.map(|ret| ret.param.tref); + let err_val = err_type + .clone() + .map(|_res| quote!(#abi_ret::from(e))) + .unwrap_or_else(|| quote!(())); + + let error_handling = |location: &str| -> TokenStream { + if let Some(tref) = &err_type { + let abi_ret = match tref.type_().passed_by() { + witx::TypePassedBy::Value(atom) => names.atom_type(atom), + _ => unreachable!("err should always be passed by value"), + }; + let err_typename = names.type_ref(&tref, anon_lifetime()); + quote! { + let e = wiggle_runtime::GuestError::InFunc { funcname: #funcname, location: #location, err: Box::new(e.into()) }; + let err: #err_typename = wiggle_runtime::GuestErrorType::from_error(e, ctx); + return #abi_ret::from(err); + } + } else { + quote! { + panic!("error: {:?}", e) + } + } + }; + + let marshal_args = func + .params + .iter() + .map(|p| marshal_arg(names, p, error_handling(p.name.as_str()))); + let trait_args = func.params.iter().map(|param| { + let name = names.func_param(¶m.name); + match param.tref.type_().passed_by() { + witx::TypePassedBy::Value { .. } => quote!(#name), + witx::TypePassedBy::Pointer { .. } => quote!(&#name), + witx::TypePassedBy::PointerLengthPair { .. } => quote!(&#name), + } + }); + + let (trait_rets, trait_bindings) = if func.results.len() < 2 { + (quote!({}), quote!(_)) + } else { + let trait_rets = func + .results + .iter() + .skip(1) + .map(|result| names.func_param(&result.name)); + let tuple = quote!((#(#trait_rets),*)); + (tuple.clone(), tuple) + }; + + // Return value pointers need to be validated before the api call, then + // assigned to afterwards. marshal_result returns these two statements as a pair. + let marshal_rets = func + .results + .iter() + .skip(1) + .map(|result| marshal_result(names, result, &error_handling)); + let marshal_rets_pre = marshal_rets.clone().map(|(pre, _post)| pre); + let marshal_rets_post = marshal_rets.map(|(_pre, post)| post); + + let success = if let Some(ref err_type) = err_type { + let err_typename = names.type_ref(&err_type, anon_lifetime()); + quote! { + let success:#err_typename = wiggle_runtime::GuestErrorType::success(); + #abi_ret::from(success) + } + } else { + quote!() + }; + + quote!(pub fn #ident(#abi_args) -> #abi_ret { + #(#marshal_args)* + #(#marshal_rets_pre)* + let #trait_bindings = match ctx.#ident(#(#trait_args),*) { + Ok(#trait_bindings) => #trait_rets, + Err(e) => { return #err_val; }, + }; + #(#marshal_rets_post)* + #success + }) +} + +fn marshal_arg( + names: &Names, + param: &witx::InterfaceFuncParam, + error_handling: TokenStream, +) -> TokenStream { + let tref = ¶m.tref; + let interface_typename = names.type_ref(&tref, anon_lifetime()); + + let try_into_conversion = { + let name = names.func_param(¶m.name); + quote! { + let #name: #interface_typename = { + use ::std::convert::TryInto; + match #name.try_into() { + Ok(a) => a, + Err(e) => { + #error_handling + } + } + }; + } + }; + + let read_conversion = { + let pointee_type = names.type_ref(tref, anon_lifetime()); + let arg_name = names.func_ptr_binding(¶m.name); + let name = names.func_param(¶m.name); + quote! { + let #name = match wiggle_runtime::GuestPtr::<#pointee_type>::new(memory, #arg_name as u32).read() { + Ok(r) => r, + Err(e) => { + #error_handling + } + }; + } + }; + + match &*tref.type_() { + witx::Type::Enum(_e) => try_into_conversion, + witx::Type::Flags(_f) => try_into_conversion, + witx::Type::Int(_i) => try_into_conversion, + witx::Type::Builtin(b) => match b { + witx::BuiltinType::U8 | witx::BuiltinType::U16 | witx::BuiltinType::Char8 => { + try_into_conversion + } + witx::BuiltinType::S8 | witx::BuiltinType::S16 => { + let name = names.func_param(¶m.name); + quote! { + let #name: #interface_typename = match (#name as i32).try_into() { + Ok(a) => a, + Err(e) => { + #error_handling + } + } + } + } + witx::BuiltinType::U32 + | witx::BuiltinType::S32 + | witx::BuiltinType::U64 + | witx::BuiltinType::S64 + | witx::BuiltinType::USize + | witx::BuiltinType::F32 + | witx::BuiltinType::F64 => { + let name = names.func_param(¶m.name); + quote! { + let #name = #name as #interface_typename; + } + } + witx::BuiltinType::String => { + let lifetime = anon_lifetime(); + let ptr_name = names.func_ptr_binding(¶m.name); + let len_name = names.func_len_binding(¶m.name); + let name = names.func_param(¶m.name); + quote! { + let #name = wiggle_runtime::GuestPtr::<#lifetime, str>::new(memory, (#ptr_name as u32, #len_name as u32)); + } + } + }, + witx::Type::Pointer(pointee) | witx::Type::ConstPointer(pointee) => { + let pointee_type = names.type_ref(pointee, anon_lifetime()); + let name = names.func_param(¶m.name); + quote! { + let #name = wiggle_runtime::GuestPtr::<#pointee_type>::new(memory, #name as u32); + } + } + witx::Type::Struct(_) => read_conversion, + witx::Type::Array(arr) => { + let pointee_type = names.type_ref(arr, anon_lifetime()); + let ptr_name = names.func_ptr_binding(¶m.name); + let len_name = names.func_len_binding(¶m.name); + let name = names.func_param(¶m.name); + quote! { + let #name = wiggle_runtime::GuestPtr::<[#pointee_type]>::new(memory, (#ptr_name as u32, #len_name as u32)); + } + } + witx::Type::Union(_u) => read_conversion, + witx::Type::Handle(_h) => { + let name = names.func_param(¶m.name); + let handle_type = names.type_ref(tref, anon_lifetime()); + quote!( let #name = #handle_type::from(#name); ) + } + } +} + +fn marshal_result( + names: &Names, + result: &witx::InterfaceFuncParam, + error_handling: F, +) -> (TokenStream, TokenStream) +where + F: Fn(&str) -> TokenStream, +{ + let tref = &result.tref; + + let write_val_to_ptr = { + let pointee_type = names.type_ref(tref, anon_lifetime()); + // core type is given func_ptr_binding name. + let ptr_name = names.func_ptr_binding(&result.name); + let ptr_err_handling = error_handling(&format!("{}:result_ptr_mut", result.name.as_str())); + let pre = quote! { + let #ptr_name = wiggle_runtime::GuestPtr::<#pointee_type>::new(memory, #ptr_name as u32); + }; + // trait binding returns func_param name. + let val_name = names.func_param(&result.name); + let post = quote! { + if let Err(e) = #ptr_name.write(#val_name) { + #ptr_err_handling + } + }; + (pre, post) + }; + + match &*tref.type_() { + witx::Type::Builtin(b) => match b { + witx::BuiltinType::String => unimplemented!("string result types"), + _ => write_val_to_ptr, + }, + witx::Type::Pointer { .. } | witx::Type::ConstPointer { .. } | witx::Type::Array { .. } => { + unimplemented!("pointer/array result types") + } + _ => write_val_to_ptr, + } +} diff --git a/crates/wiggle/crates/generate/src/lib.rs b/crates/wiggle/crates/generate/src/lib.rs new file mode 100644 index 0000000000..d253f2604e --- /dev/null +++ b/crates/wiggle/crates/generate/src/lib.rs @@ -0,0 +1,44 @@ +pub mod config; +mod funcs; +mod lifetimes; +mod module_trait; +mod names; +mod types; + +use proc_macro2::TokenStream; +use quote::quote; + +pub use config::Config; +pub use funcs::define_func; +pub use module_trait::define_module_trait; +pub use names::Names; +pub use types::define_datatype; + +pub fn generate(doc: &witx::Document, config: &Config) -> TokenStream { + let names = Names::new(config); // TODO parse the names from the invocation of the macro, or from a file? + + let types = doc.typenames().map(|t| define_datatype(&names, &t)); + + let modules = doc.modules().map(|module| { + let modname = names.module(&module.name); + let fs = module.funcs().map(|f| define_func(&names, &f)); + let modtrait = define_module_trait(&names, &module); + let ctx_type = names.ctx_type(); + quote!( + pub mod #modname { + use super::#ctx_type; + use super::types::*; + #(#fs)* + + #modtrait + } + ) + }); + + quote!( + pub mod types { + #(#types)* + } + #(#modules)* + ) +} diff --git a/crates/wiggle/crates/generate/src/lifetimes.rs b/crates/wiggle/crates/generate/src/lifetimes.rs new file mode 100644 index 0000000000..75b102209c --- /dev/null +++ b/crates/wiggle/crates/generate/src/lifetimes.rs @@ -0,0 +1,83 @@ +use proc_macro2::TokenStream; +use quote::quote; + +pub trait LifetimeExt { + fn is_transparent(&self) -> bool; + fn needs_lifetime(&self) -> bool; +} + +impl LifetimeExt for witx::TypeRef { + fn is_transparent(&self) -> bool { + self.type_().is_transparent() + } + fn needs_lifetime(&self) -> bool { + self.type_().needs_lifetime() + } +} + +impl LifetimeExt for witx::Type { + fn is_transparent(&self) -> bool { + match self { + witx::Type::Builtin(b) => b.is_transparent(), + witx::Type::Struct(s) => s.is_transparent(), + witx::Type::Enum { .. } + | witx::Type::Flags { .. } + | witx::Type::Int { .. } + | witx::Type::Handle { .. } => true, + witx::Type::Union { .. } + | witx::Type::Pointer { .. } + | witx::Type::ConstPointer { .. } + | witx::Type::Array { .. } => false, + } + } + fn needs_lifetime(&self) -> bool { + match self { + witx::Type::Builtin(b) => b.needs_lifetime(), + witx::Type::Struct(s) => s.needs_lifetime(), + witx::Type::Union(u) => u.needs_lifetime(), + witx::Type::Enum { .. } + | witx::Type::Flags { .. } + | witx::Type::Int { .. } + | witx::Type::Handle { .. } => false, + witx::Type::Pointer { .. } + | witx::Type::ConstPointer { .. } + | witx::Type::Array { .. } => true, + } + } +} + +impl LifetimeExt for witx::BuiltinType { + fn is_transparent(&self) -> bool { + !self.needs_lifetime() + } + fn needs_lifetime(&self) -> bool { + match self { + witx::BuiltinType::String => true, + _ => false, + } + } +} + +impl LifetimeExt for witx::StructDatatype { + fn is_transparent(&self) -> bool { + self.members.iter().all(|m| m.tref.is_transparent()) + } + fn needs_lifetime(&self) -> bool { + self.members.iter().any(|m| m.tref.needs_lifetime()) + } +} + +impl LifetimeExt for witx::UnionDatatype { + fn is_transparent(&self) -> bool { + false + } + fn needs_lifetime(&self) -> bool { + self.variants + .iter() + .any(|m| m.tref.as_ref().map(|t| t.needs_lifetime()).unwrap_or(false)) + } +} + +pub fn anon_lifetime() -> TokenStream { + quote!('_) +} diff --git a/crates/wiggle/crates/generate/src/module_trait.rs b/crates/wiggle/crates/generate/src/module_trait.rs new file mode 100644 index 0000000000..bd94946bba --- /dev/null +++ b/crates/wiggle/crates/generate/src/module_trait.rs @@ -0,0 +1,57 @@ +use proc_macro2::TokenStream; +use quote::quote; + +use crate::lifetimes::{anon_lifetime, LifetimeExt}; +use crate::names::Names; +use witx::Module; + +pub fn define_module_trait(names: &Names, m: &Module) -> TokenStream { + let traitname = names.trait_name(&m.name); + let traitmethods = m.funcs().map(|f| { + // Check if we're returning an entity anotated with a lifetime, + // in which case, we'll need to annotate the function itself, and + // hence will need an explicit lifetime (rather than anonymous) + let (lifetime, is_anonymous) = if f + .params + .iter() + .chain(&f.results) + .any(|ret| ret.tref.needs_lifetime()) + { + (quote!('a), false) + } else { + (anon_lifetime(), true) + }; + let funcname = names.func(&f.name); + let args = f.params.iter().map(|arg| { + let arg_name = names.func_param(&arg.name); + let arg_typename = names.type_ref(&arg.tref, lifetime.clone()); + let arg_type = match arg.tref.type_().passed_by() { + witx::TypePassedBy::Value { .. } => quote!(#arg_typename), + witx::TypePassedBy::Pointer { .. } => quote!(&#arg_typename), + witx::TypePassedBy::PointerLengthPair { .. } => quote!(&#arg_typename), + }; + quote!(#arg_name: #arg_type) + }); + let rets = f + .results + .iter() + .skip(1) + .map(|ret| names.type_ref(&ret.tref, lifetime.clone())); + let err = f + .results + .get(0) + .map(|err_result| names.type_ref(&err_result.tref, lifetime.clone())) + .unwrap_or(quote!(())); + + if is_anonymous { + quote!(fn #funcname(&self, #(#args),*) -> Result<(#(#rets),*), #err>;) + } else { + quote!(fn #funcname<#lifetime>(&self, #(#args),*) -> Result<(#(#rets),*), #err>;) + } + }); + quote! { + pub trait #traitname { + #(#traitmethods)* + } + } +} diff --git a/crates/wiggle/crates/generate/src/names.rs b/crates/wiggle/crates/generate/src/names.rs new file mode 100644 index 0000000000..d545b37845 --- /dev/null +++ b/crates/wiggle/crates/generate/src/names.rs @@ -0,0 +1,140 @@ +use heck::{CamelCase, ShoutySnakeCase, SnakeCase}; +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; +use witx::{AtomType, BuiltinType, Id, TypeRef}; + +use crate::lifetimes::LifetimeExt; +use crate::Config; + +#[derive(Debug, Clone)] +pub struct Names { + config: Config, +} + +impl Names { + pub fn new(config: &Config) -> Names { + Names { + config: config.clone(), + } + } + pub fn ctx_type(&self) -> Ident { + self.config.ctx.name.clone() + } + pub fn type_(&self, id: &Id) -> TokenStream { + let ident = format_ident!("{}", id.as_str().to_camel_case()); + quote!(#ident) + } + pub fn builtin_type(&self, b: BuiltinType, lifetime: TokenStream) -> TokenStream { + match b { + BuiltinType::String => quote!(wiggle_runtime::GuestPtr<#lifetime, str>), + BuiltinType::U8 => quote!(u8), + BuiltinType::U16 => quote!(u16), + BuiltinType::U32 => quote!(u32), + BuiltinType::U64 => quote!(u64), + BuiltinType::S8 => quote!(i8), + BuiltinType::S16 => quote!(i16), + BuiltinType::S32 => quote!(i32), + BuiltinType::S64 => quote!(i64), + BuiltinType::F32 => quote!(f32), + BuiltinType::F64 => quote!(f64), + BuiltinType::Char8 => quote!(u8), + BuiltinType::USize => quote!(usize), + } + } + pub fn atom_type(&self, atom: AtomType) -> TokenStream { + match atom { + AtomType::I32 => quote!(i32), + AtomType::I64 => quote!(i64), + AtomType::F32 => quote!(f32), + AtomType::F64 => quote!(f64), + } + } + + pub fn type_ref(&self, tref: &TypeRef, lifetime: TokenStream) -> TokenStream { + match tref { + TypeRef::Name(nt) => { + let ident = self.type_(&nt.name); + if nt.tref.needs_lifetime() { + quote!(#ident<#lifetime>) + } else { + quote!(#ident) + } + } + TypeRef::Value(ty) => match &**ty { + witx::Type::Builtin(builtin) => self.builtin_type(*builtin, lifetime.clone()), + witx::Type::Pointer(pointee) | witx::Type::ConstPointer(pointee) => { + let pointee_type = self.type_ref(&pointee, lifetime.clone()); + quote!(wiggle_runtime::GuestPtr<#lifetime, #pointee_type>) + } + _ => unimplemented!("anonymous type ref"), + }, + } + } + + pub fn enum_variant(&self, id: &Id) -> Ident { + // FIXME this is a hack - just a proof of concept. + if id.as_str().starts_with('2') { + format_ident!("TooBig") + } else if id.as_str() == "type" { + format_ident!("Type") + } else { + format_ident!("{}", id.as_str().to_camel_case()) + } + } + + pub fn flag_member(&self, id: &Id) -> Ident { + format_ident!("{}", id.as_str().to_shouty_snake_case()) + } + + pub fn int_member(&self, id: &Id) -> Ident { + format_ident!("{}", id.as_str().to_shouty_snake_case()) + } + + pub fn struct_member(&self, id: &Id) -> Ident { + // FIXME this is a hack - just a proof of concept. + if id.as_str() == "type" { + format_ident!("type_") + } else { + format_ident!("{}", id.as_str().to_snake_case()) + } + } + + pub fn module(&self, id: &Id) -> Ident { + format_ident!("{}", id.as_str().to_snake_case()) + } + + pub fn trait_name(&self, id: &Id) -> Ident { + format_ident!("{}", id.as_str().to_camel_case()) + } + + pub fn func(&self, id: &Id) -> Ident { + format_ident!("{}", id.as_str().to_snake_case()) + } + + pub fn func_param(&self, id: &Id) -> Ident { + // FIXME this is a hack - just a proof of concept. + if id.as_str() == "in" { + format_ident!("in_") + } else { + format_ident!("{}", id.as_str().to_snake_case()) + } + } + + pub fn func_core_arg(&self, arg: &witx::CoreParamType) -> Ident { + match arg.signifies { + witx::CoreParamSignifies::Value { .. } => self.func_param(&arg.param.name), + witx::CoreParamSignifies::PointerTo => self.func_ptr_binding(&arg.param.name), + witx::CoreParamSignifies::LengthOf => self.func_len_binding(&arg.param.name), + } + } + + /// For when you need a {name}_ptr binding for passing a value by reference: + pub fn func_ptr_binding(&self, id: &Id) -> Ident { + format_ident!("{}_ptr", id.as_str().to_snake_case()) + } + + /// For when you need a {name}_len binding for passing an array: + pub fn func_len_binding(&self, id: &Id) -> Ident { + format_ident!("{}_len", id.as_str().to_snake_case()) + } +} diff --git a/crates/wiggle/crates/generate/src/types/enum.rs b/crates/wiggle/crates/generate/src/types/enum.rs new file mode 100644 index 0000000000..8aa77f8682 --- /dev/null +++ b/crates/wiggle/crates/generate/src/types/enum.rs @@ -0,0 +1,113 @@ +use super::{atom_token, int_repr_tokens}; +use crate::names::Names; + +use proc_macro2::TokenStream; +use quote::quote; + +pub(super) fn define_enum(names: &Names, name: &witx::Id, e: &witx::EnumDatatype) -> TokenStream { + let ident = names.type_(&name); + + let repr = int_repr_tokens(e.repr); + let abi_repr = atom_token(match e.repr { + witx::IntRepr::U8 | witx::IntRepr::U16 | witx::IntRepr::U32 => witx::AtomType::I32, + witx::IntRepr::U64 => witx::AtomType::I64, + }); + + let mut variant_names = vec![]; + let mut tryfrom_repr_cases = vec![]; + let mut to_repr_cases = vec![]; + let mut to_display = vec![]; + + for (n, variant) in e.variants.iter().enumerate() { + let variant_name = names.enum_variant(&variant.name); + let docs = variant.docs.trim(); + let ident_str = ident.to_string(); + let variant_str = variant_name.to_string(); + tryfrom_repr_cases.push(quote!(#n => Ok(#ident::#variant_name))); + to_repr_cases.push(quote!(#ident::#variant_name => #n as #repr)); + to_display.push(quote!(#ident::#variant_name => format!("{} ({}::{}({}))", #docs, #ident_str, #variant_str, #repr::from(*self)))); + variant_names.push(variant_name); + } + + quote! { + #[repr(#repr)] + #[derive(Copy, Clone, Debug, ::std::hash::Hash, Eq, PartialEq)] + pub enum #ident { + #(#variant_names),* + } + + impl ::std::fmt::Display for #ident { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + let to_str = match self { + #(#to_display,)* + }; + write!(f, "{}", to_str) + } + } + + impl ::std::convert::TryFrom<#repr> for #ident { + type Error = wiggle_runtime::GuestError; + fn try_from(value: #repr) -> Result<#ident, wiggle_runtime::GuestError> { + match value as usize { + #(#tryfrom_repr_cases),*, + _ => Err(wiggle_runtime::GuestError::InvalidEnumValue(stringify!(#ident))), + } + } + } + + impl ::std::convert::TryFrom<#abi_repr> for #ident { + type Error = wiggle_runtime::GuestError; + fn try_from(value: #abi_repr) -> Result<#ident, wiggle_runtime::GuestError> { + #ident::try_from(value as #repr) + } + } + + impl From<#ident> for #repr { + fn from(e: #ident) -> #repr { + match e { + #(#to_repr_cases),* + } + } + } + + impl From<#ident> for #abi_repr { + fn from(e: #ident) -> #abi_repr { + #repr::from(e) as #abi_repr + } + } + + impl<'a> wiggle_runtime::GuestType<'a> for #ident { + fn guest_size() -> u32 { + #repr::guest_size() + } + + fn guest_align() -> usize { + #repr::guest_align() + } + + fn read(location: &wiggle_runtime::GuestPtr<#ident>) -> Result<#ident, wiggle_runtime::GuestError> { + use std::convert::TryFrom; + let reprval = #repr::read(&location.cast())?; + let value = #ident::try_from(reprval)?; + Ok(value) + } + + fn write(location: &wiggle_runtime::GuestPtr<'_, #ident>, val: Self) + -> Result<(), wiggle_runtime::GuestError> + { + #repr::write(&location.cast(), #repr::from(val)) + } + } + + unsafe impl <'a> wiggle_runtime::GuestTypeTransparent<'a> for #ident { + #[inline] + fn validate(location: *mut #ident) -> Result<(), wiggle_runtime::GuestError> { + use std::convert::TryFrom; + // Validate value in memory using #ident::try_from(reprval) + let reprval = unsafe { (location as *mut #repr).read() }; + let _val = #ident::try_from(reprval)?; + Ok(()) + } + } + } +} diff --git a/crates/wiggle/crates/generate/src/types/flags.rs b/crates/wiggle/crates/generate/src/types/flags.rs new file mode 100644 index 0000000000..60aa671310 --- /dev/null +++ b/crates/wiggle/crates/generate/src/types/flags.rs @@ -0,0 +1,161 @@ +use super::{atom_token, int_repr_tokens}; +use crate::names::Names; + +use proc_macro2::{Literal, TokenStream}; +use quote::quote; +use std::convert::TryFrom; + +pub(super) fn define_flags(names: &Names, name: &witx::Id, f: &witx::FlagsDatatype) -> TokenStream { + let ident = names.type_(&name); + let repr = int_repr_tokens(f.repr); + let abi_repr = atom_token(match f.repr { + witx::IntRepr::U8 | witx::IntRepr::U16 | witx::IntRepr::U32 => witx::AtomType::I32, + witx::IntRepr::U64 => witx::AtomType::I64, + }); + + let mut flag_constructors = vec![]; + let mut all_values = 0; + for (i, f) in f.flags.iter().enumerate() { + let name = names.flag_member(&f.name); + let value = 1u128 + .checked_shl(u32::try_from(i).expect("flag value overflow")) + .expect("flag value overflow"); + let value_token = Literal::u128_unsuffixed(value); + flag_constructors.push(quote!(pub const #name: #ident = #ident(#value_token))); + all_values += value; + } + let all_values_token = Literal::u128_unsuffixed(all_values); + + let ident_str = ident.to_string(); + + quote! { + #[repr(transparent)] + #[derive(Copy, Clone, Debug, ::std::hash::Hash, Eq, PartialEq)] + pub struct #ident(#repr); + + impl #ident { + #(#flag_constructors);*; + pub const EMPTY_FLAGS: #ident = #ident(0 as #repr); + pub const ALL_FLAGS: #ident = #ident(#all_values_token); + + pub fn contains(&self, other: &#ident) -> bool { + !*self & *other == Self::EMPTY_FLAGS + } + } + + impl ::std::fmt::Display for #ident { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{}({:#b})", #ident_str, self.0) + } + } + + impl ::std::ops::BitAnd for #ident { + type Output = Self; + fn bitand(self, rhs: Self) -> Self::Output { + #ident(self.0 & rhs.0) + } + } + + impl ::std::ops::BitAndAssign for #ident { + fn bitand_assign(&mut self, rhs: Self) { + *self = *self & rhs + } + } + + impl ::std::ops::BitOr for #ident { + type Output = Self; + fn bitor(self, rhs: Self) -> Self::Output { + #ident(self.0 | rhs.0) + } + } + + impl ::std::ops::BitOrAssign for #ident { + fn bitor_assign(&mut self, rhs: Self) { + *self = *self | rhs + } + } + + impl ::std::ops::BitXor for #ident { + type Output = Self; + fn bitxor(self, rhs: Self) -> Self::Output { + #ident(self.0 ^ rhs.0) + } + } + + impl ::std::ops::BitXorAssign for #ident { + fn bitxor_assign(&mut self, rhs: Self) { + *self = *self ^ rhs + } + } + + impl ::std::ops::Not for #ident { + type Output = Self; + fn not(self) -> Self::Output { + #ident(!self.0) + } + } + + impl ::std::convert::TryFrom<#repr> for #ident { + type Error = wiggle_runtime::GuestError; + fn try_from(value: #repr) -> Result { + if #repr::from(!#ident::ALL_FLAGS) & value != 0 { + Err(wiggle_runtime::GuestError::InvalidFlagValue(stringify!(#ident))) + } else { + Ok(#ident(value)) + } + } + } + + impl ::std::convert::TryFrom<#abi_repr> for #ident { + type Error = wiggle_runtime::GuestError; + fn try_from(value: #abi_repr) -> Result<#ident, wiggle_runtime::GuestError> { + #ident::try_from(value as #repr) + } + } + + impl From<#ident> for #repr { + fn from(e: #ident) -> #repr { + e.0 + } + } + + impl From<#ident> for #abi_repr { + fn from(e: #ident) -> #abi_repr { + #repr::from(e) as #abi_repr + } + } + + impl<'a> wiggle_runtime::GuestType<'a> for #ident { + fn guest_size() -> u32 { + #repr::guest_size() + } + + fn guest_align() -> usize { + #repr::guest_align() + } + + fn read(location: &wiggle_runtime::GuestPtr<#ident>) -> Result<#ident, wiggle_runtime::GuestError> { + use std::convert::TryFrom; + let reprval = #repr::read(&location.cast())?; + let value = #ident::try_from(reprval)?; + Ok(value) + } + + fn write(location: &wiggle_runtime::GuestPtr<'_, #ident>, val: Self) -> Result<(), wiggle_runtime::GuestError> { + let val: #repr = #repr::from(val); + #repr::write(&location.cast(), val) + } + } + unsafe impl <'a> wiggle_runtime::GuestTypeTransparent<'a> for #ident { + #[inline] + fn validate(location: *mut #ident) -> Result<(), wiggle_runtime::GuestError> { + use std::convert::TryFrom; + // Validate value in memory using #ident::try_from(reprval) + let reprval = unsafe { (location as *mut #repr).read() }; + let _val = #ident::try_from(reprval)?; + Ok(()) + } + } + + } +} diff --git a/crates/wiggle/crates/generate/src/types/handle.rs b/crates/wiggle/crates/generate/src/types/handle.rs new file mode 100644 index 0000000000..4f922cbd7b --- /dev/null +++ b/crates/wiggle/crates/generate/src/types/handle.rs @@ -0,0 +1,77 @@ +use crate::names::Names; + +use proc_macro2::TokenStream; +use quote::quote; +use witx::Layout; + +pub(super) fn define_handle( + names: &Names, + name: &witx::Id, + h: &witx::HandleDatatype, +) -> TokenStream { + let ident = names.type_(name); + let size = h.mem_size_align().size as u32; + let align = h.mem_size_align().align as usize; + quote! { + #[repr(transparent)] + #[derive(Copy, Clone, Debug, ::std::hash::Hash, Eq, PartialEq)] + pub struct #ident(u32); + + impl From<#ident> for u32 { + fn from(e: #ident) -> u32 { + e.0 + } + } + + impl From<#ident> for i32 { + fn from(e: #ident) -> i32 { + e.0 as i32 + } + } + + impl From for #ident { + fn from(e: u32) -> #ident { + #ident(e) + } + } + impl From for #ident { + fn from(e: i32) -> #ident { + #ident(e as u32) + } + } + + impl ::std::fmt::Display for #ident { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{}({})", stringify!(#ident), self.0) + } + } + + impl<'a> wiggle_runtime::GuestType<'a> for #ident { + fn guest_size() -> u32 { + #size + } + + fn guest_align() -> usize { + #align + } + + fn read(location: &wiggle_runtime::GuestPtr<'a, #ident>) -> Result<#ident, wiggle_runtime::GuestError> { + Ok(#ident(u32::read(&location.cast())?)) + } + + fn write(location: &wiggle_runtime::GuestPtr<'_, Self>, val: Self) -> Result<(), wiggle_runtime::GuestError> { + u32::write(&location.cast(), val.0) + } + } + + unsafe impl<'a> wiggle_runtime::GuestTypeTransparent<'a> for #ident { + #[inline] + fn validate(_location: *mut #ident) -> Result<(), wiggle_runtime::GuestError> { + // All bit patterns accepted + Ok(()) + } + } + + + } +} diff --git a/crates/wiggle/crates/generate/src/types/int.rs b/crates/wiggle/crates/generate/src/types/int.rs new file mode 100644 index 0000000000..ee870eb6a6 --- /dev/null +++ b/crates/wiggle/crates/generate/src/types/int.rs @@ -0,0 +1,93 @@ +use super::{atom_token, int_repr_tokens}; +use crate::names::Names; + +use proc_macro2::TokenStream; +use quote::quote; + +pub(super) fn define_int(names: &Names, name: &witx::Id, i: &witx::IntDatatype) -> TokenStream { + let ident = names.type_(&name); + let repr = int_repr_tokens(i.repr); + let abi_repr = atom_token(match i.repr { + witx::IntRepr::U8 | witx::IntRepr::U16 | witx::IntRepr::U32 => witx::AtomType::I32, + witx::IntRepr::U64 => witx::AtomType::I64, + }); + let consts = i + .consts + .iter() + .map(|r#const| { + let const_ident = names.int_member(&r#const.name); + let value = r#const.value; + quote!(pub const #const_ident: #ident = #ident(#value)) + }) + .collect::>(); + + quote! { + #[repr(transparent)] + #[derive(Copy, Clone, Debug, ::std::hash::Hash, Eq, PartialEq)] + pub struct #ident(#repr); + + impl #ident { + #(#consts;)* + } + + impl ::std::fmt::Display for #ident { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{:?}", self) + } + } + + impl ::std::convert::TryFrom<#repr> for #ident { + type Error = wiggle_runtime::GuestError; + fn try_from(value: #repr) -> Result { + Ok(#ident(value)) + } + } + + impl ::std::convert::TryFrom<#abi_repr> for #ident { + type Error = wiggle_runtime::GuestError; + fn try_from(value: #abi_repr) -> Result<#ident, wiggle_runtime::GuestError> { + #ident::try_from(value as #repr) + } + } + + impl From<#ident> for #repr { + fn from(e: #ident) -> #repr { + e.0 + } + } + + impl From<#ident> for #abi_repr { + fn from(e: #ident) -> #abi_repr { + #repr::from(e) as #abi_repr + } + } + + impl<'a> wiggle_runtime::GuestType<'a> for #ident { + fn guest_size() -> u32 { + #repr::guest_size() + } + + fn guest_align() -> usize { + #repr::guest_align() + } + + fn read(location: &wiggle_runtime::GuestPtr<'a, #ident>) -> Result<#ident, wiggle_runtime::GuestError> { + Ok(#ident(#repr::read(&location.cast())?)) + + } + + fn write(location: &wiggle_runtime::GuestPtr<'_, #ident>, val: Self) -> Result<(), wiggle_runtime::GuestError> { + #repr::write(&location.cast(), val.0) + } + } + + unsafe impl<'a> wiggle_runtime::GuestTypeTransparent<'a> for #ident { + #[inline] + fn validate(_location: *mut #ident) -> Result<(), wiggle_runtime::GuestError> { + // All bit patterns accepted + Ok(()) + } + } + + } +} diff --git a/crates/wiggle/crates/generate/src/types/mod.rs b/crates/wiggle/crates/generate/src/types/mod.rs new file mode 100644 index 0000000000..93e7d5f892 --- /dev/null +++ b/crates/wiggle/crates/generate/src/types/mod.rs @@ -0,0 +1,90 @@ +mod r#enum; +mod flags; +mod handle; +mod int; +mod r#struct; +mod union; + +use crate::lifetimes::LifetimeExt; +use crate::names::Names; + +use proc_macro2::TokenStream; +use quote::quote; + +pub fn define_datatype(names: &Names, namedtype: &witx::NamedType) -> TokenStream { + match &namedtype.tref { + witx::TypeRef::Name(alias_to) => define_alias(names, &namedtype.name, &alias_to), + witx::TypeRef::Value(v) => match &**v { + witx::Type::Enum(e) => r#enum::define_enum(names, &namedtype.name, &e), + witx::Type::Int(i) => int::define_int(names, &namedtype.name, &i), + witx::Type::Flags(f) => flags::define_flags(names, &namedtype.name, &f), + witx::Type::Struct(s) => r#struct::define_struct(names, &namedtype.name, &s), + witx::Type::Union(u) => union::define_union(names, &namedtype.name, &u), + witx::Type::Handle(h) => handle::define_handle(names, &namedtype.name, &h), + witx::Type::Builtin(b) => define_builtin(names, &namedtype.name, *b), + witx::Type::Pointer(p) => { + define_witx_pointer(names, &namedtype.name, quote!(wiggle_runtime::GuestPtr), p) + } + witx::Type::ConstPointer(p) => { + define_witx_pointer(names, &namedtype.name, quote!(wiggle_runtime::GuestPtr), p) + } + witx::Type::Array(arr) => define_witx_array(names, &namedtype.name, &arr), + }, + } +} + +fn define_alias(names: &Names, name: &witx::Id, to: &witx::NamedType) -> TokenStream { + let ident = names.type_(name); + let rhs = names.type_(&to.name); + if to.tref.needs_lifetime() { + quote!(pub type #ident<'a> = #rhs<'a>;) + } else { + quote!(pub type #ident = #rhs;) + } +} + +fn define_builtin(names: &Names, name: &witx::Id, builtin: witx::BuiltinType) -> TokenStream { + let ident = names.type_(name); + let built = names.builtin_type(builtin, quote!('a)); + if builtin.needs_lifetime() { + quote!(pub type #ident<'a> = #built;) + } else { + quote!(pub type #ident = #built;) + } +} + +fn define_witx_pointer( + names: &Names, + name: &witx::Id, + pointer_type: TokenStream, + pointee: &witx::TypeRef, +) -> TokenStream { + let ident = names.type_(name); + let pointee_type = names.type_ref(pointee, quote!('a)); + + quote!(pub type #ident<'a> = #pointer_type<'a, #pointee_type>;) +} + +fn define_witx_array(names: &Names, name: &witx::Id, arr_raw: &witx::TypeRef) -> TokenStream { + let ident = names.type_(name); + let pointee_type = names.type_ref(arr_raw, quote!('a)); + quote!(pub type #ident<'a> = wiggle_runtime::GuestPtr<'a, [#pointee_type]>;) +} + +fn int_repr_tokens(int_repr: witx::IntRepr) -> TokenStream { + match int_repr { + witx::IntRepr::U8 => quote!(u8), + witx::IntRepr::U16 => quote!(u16), + witx::IntRepr::U32 => quote!(u32), + witx::IntRepr::U64 => quote!(u64), + } +} + +fn atom_token(atom: witx::AtomType) -> TokenStream { + match atom { + witx::AtomType::I32 => quote!(i32), + witx::AtomType::I64 => quote!(i64), + witx::AtomType::F32 => quote!(f32), + witx::AtomType::F64 => quote!(f64), + } +} diff --git a/crates/wiggle/crates/generate/src/types/struct.rs b/crates/wiggle/crates/generate/src/types/struct.rs new file mode 100644 index 0000000000..d0c3b03cdd --- /dev/null +++ b/crates/wiggle/crates/generate/src/types/struct.rs @@ -0,0 +1,134 @@ +use crate::lifetimes::{anon_lifetime, LifetimeExt}; +use crate::names::Names; + +use proc_macro2::TokenStream; +use quote::quote; +use witx::Layout; + +pub(super) fn define_struct( + names: &Names, + name: &witx::Id, + s: &witx::StructDatatype, +) -> TokenStream { + let ident = names.type_(name); + let size = s.mem_size_align().size as u32; + let align = s.mem_size_align().align as usize; + + let member_names = s.members.iter().map(|m| names.struct_member(&m.name)); + let member_decls = s.members.iter().map(|m| { + let name = names.struct_member(&m.name); + let type_ = match &m.tref { + witx::TypeRef::Name(nt) => names.type_(&nt.name), + witx::TypeRef::Value(ty) => match &**ty { + witx::Type::Builtin(builtin) => names.builtin_type(*builtin, quote!('a)), + witx::Type::Pointer(pointee) | witx::Type::ConstPointer(pointee) => { + let pointee_type = names.type_ref(&pointee, quote!('a)); + quote!(wiggle_runtime::GuestPtr<'a, #pointee_type>) + } + _ => unimplemented!("other anonymous struct members"), + }, + }; + quote!(pub #name: #type_) + }); + + let member_reads = s.member_layout().into_iter().map(|ml| { + let name = names.struct_member(&ml.member.name); + let offset = ml.offset as u32; + let location = quote!(location.cast::().add(#offset)?.cast()); + match &ml.member.tref { + witx::TypeRef::Name(nt) => { + let type_ = names.type_(&nt.name); + quote! { + let #name = <#type_ as wiggle_runtime::GuestType>::read(&#location)?; + } + } + witx::TypeRef::Value(ty) => match &**ty { + witx::Type::Builtin(builtin) => { + let type_ = names.builtin_type(*builtin, anon_lifetime()); + quote! { + let #name = <#type_ as wiggle_runtime::GuestType>::read(&#location)?; + } + } + witx::Type::Pointer(pointee) | witx::Type::ConstPointer(pointee) => { + let pointee_type = names.type_ref(&pointee, anon_lifetime()); + quote! { + let #name = as wiggle_runtime::GuestType>::read(&#location)?; + } + } + _ => unimplemented!("other anonymous struct members"), + }, + } + }); + + let member_writes = s.member_layout().into_iter().map(|ml| { + let name = names.struct_member(&ml.member.name); + let offset = ml.offset as u32; + quote! { + wiggle_runtime::GuestType::write( + &location.cast::().add(#offset)?.cast(), + val.#name, + )?; + } + }); + + let (struct_lifetime, extra_derive) = if s.needs_lifetime() { + (quote!(<'a>), quote!()) + } else { + (quote!(), quote!(, Copy, PartialEq)) + }; + + let transparent = if s.is_transparent() { + let member_validate = s.member_layout().into_iter().map(|ml| { + let offset = ml.offset; + let typename = names.type_ref(&ml.member.tref, anon_lifetime()); + quote! { + // SAFETY: caller has validated bounds and alignment of `location`. + // member_layout gives correctly-aligned pointers inside that area. + #typename::validate( + unsafe { (location as *mut u8).add(#offset) as *mut _ } + )?; + } + }); + + quote! { + unsafe impl<'a> wiggle_runtime::GuestTypeTransparent<'a> for #ident { + #[inline] + fn validate(location: *mut #ident) -> Result<(), wiggle_runtime::GuestError> { + #(#member_validate)* + Ok(()) + } + } + } + } else { + quote!() + }; + + quote! { + #[derive(Clone, Debug #extra_derive)] + pub struct #ident #struct_lifetime { + #(#member_decls),* + } + + impl<'a> wiggle_runtime::GuestType<'a> for #ident #struct_lifetime { + fn guest_size() -> u32 { + #size + } + + fn guest_align() -> usize { + #align + } + + fn read(location: &wiggle_runtime::GuestPtr<'a, Self>) -> Result { + #(#member_reads)* + Ok(#ident { #(#member_names),* }) + } + + fn write(location: &wiggle_runtime::GuestPtr<'_, Self>, val: Self) -> Result<(), wiggle_runtime::GuestError> { + #(#member_writes)* + Ok(()) + } + } + + #transparent + } +} diff --git a/crates/wiggle/crates/generate/src/types/union.rs b/crates/wiggle/crates/generate/src/types/union.rs new file mode 100644 index 0000000000..4490690050 --- /dev/null +++ b/crates/wiggle/crates/generate/src/types/union.rs @@ -0,0 +1,109 @@ +use crate::lifetimes::LifetimeExt; +use crate::names::Names; + +use proc_macro2::TokenStream; +use quote::quote; +use witx::Layout; + +pub(super) fn define_union(names: &Names, name: &witx::Id, u: &witx::UnionDatatype) -> TokenStream { + let ident = names.type_(name); + let size = u.mem_size_align().size as u32; + let align = u.mem_size_align().align as usize; + let ulayout = u.union_layout(); + let contents_offset = ulayout.contents_offset as u32; + + let lifetime = quote!('a); + + let variants = u.variants.iter().map(|v| { + let var_name = names.enum_variant(&v.name); + if let Some(tref) = &v.tref { + let var_type = names.type_ref(&tref, lifetime.clone()); + quote!(#var_name(#var_type)) + } else { + quote!(#var_name) + } + }); + + let tagname = names.type_(&u.tag.name); + + let read_variant = u.variants.iter().map(|v| { + let variantname = names.enum_variant(&v.name); + if let Some(tref) = &v.tref { + let varianttype = names.type_ref(tref, lifetime.clone()); + quote! { + #tagname::#variantname => { + let variant_ptr = location.cast::().add(#contents_offset)?; + let variant_val = <#varianttype as wiggle_runtime::GuestType>::read(&variant_ptr.cast())?; + Ok(#ident::#variantname(variant_val)) + } + } + } else { + quote! { #tagname::#variantname => Ok(#ident::#variantname), } + } + }); + + let write_variant = u.variants.iter().map(|v| { + let variantname = names.enum_variant(&v.name); + let write_tag = quote! { + location.cast().write(#tagname::#variantname)?; + }; + if let Some(tref) = &v.tref { + let varianttype = names.type_ref(tref, lifetime.clone()); + quote! { + #ident::#variantname(contents) => { + #write_tag + let variant_ptr = location.cast::().add(#contents_offset)?; + <#varianttype as wiggle_runtime::GuestType>::write(&variant_ptr.cast(), contents)?; + } + } + } else { + quote! { + #ident::#variantname => { + #write_tag + } + } + } + }); + + let (enum_lifetime, extra_derive) = if u.needs_lifetime() { + (quote!(<'a>), quote!()) + } else { + (quote!(), quote!(, Copy, PartialEq)) + }; + + quote! { + #[derive(Clone, Debug #extra_derive)] + pub enum #ident #enum_lifetime { + #(#variants),* + } + + impl<'a> wiggle_runtime::GuestType<'a> for #ident #enum_lifetime { + fn guest_size() -> u32 { + #size + } + + fn guest_align() -> usize { + #align + } + + fn read(location: &wiggle_runtime::GuestPtr<'a, Self>) + -> Result + { + let tag = location.cast().read()?; + match tag { + #(#read_variant)* + } + + } + + fn write(location: &wiggle_runtime::GuestPtr<'_, Self>, val: Self) + -> Result<(), wiggle_runtime::GuestError> + { + match val { + #(#write_variant)* + } + Ok(()) + } + } + } +} diff --git a/crates/wiggle/crates/runtime/.gitignore b/crates/wiggle/crates/runtime/.gitignore new file mode 100644 index 0000000000..a9d37c560c --- /dev/null +++ b/crates/wiggle/crates/runtime/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/crates/wiggle/crates/runtime/Cargo.toml b/crates/wiggle/crates/runtime/Cargo.toml new file mode 100644 index 0000000000..4e711424de --- /dev/null +++ b/crates/wiggle/crates/runtime/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "wiggle-runtime" +version = "0.1.0" +authors = ["Pat Hickey ", "Jakub Konka "] +edition = "2018" + +[dependencies] +thiserror = "1" diff --git a/crates/wiggle/crates/runtime/src/borrow.rs b/crates/wiggle/crates/runtime/src/borrow.rs new file mode 100644 index 0000000000..8d5d81c01a --- /dev/null +++ b/crates/wiggle/crates/runtime/src/borrow.rs @@ -0,0 +1,126 @@ +use crate::region::Region; +use crate::{GuestError, GuestPtr, GuestType}; + +#[derive(Debug)] +pub struct GuestBorrows { + borrows: Vec, +} + +impl GuestBorrows { + pub fn new() -> Self { + Self { + borrows: Vec::new(), + } + } + + fn is_borrowed(&self, r: Region) -> bool { + !self.borrows.iter().all(|b| !b.overlaps(r)) + } + + pub(crate) fn borrow(&mut self, r: Region) -> Result<(), GuestError> { + if self.is_borrowed(r) { + Err(GuestError::PtrBorrowed(r)) + } else { + self.borrows.push(r); + Ok(()) + } + } + + /// Borrow the region of memory pointed to by a `GuestPtr`. This is required for safety if + /// you are dereferencing `GuestPtr`s while holding a reference to a slice via + /// `GuestPtr::as_raw`. + pub fn borrow_pointee<'a, T>(&mut self, p: &GuestPtr<'a, T>) -> Result<(), GuestError> + where + T: GuestType<'a>, + { + self.borrow(Region { + start: p.offset(), + len: T::guest_size(), + }) + } + + /// Borrow the slice of memory pointed to by a `GuestPtr<[T]>`. This is required for safety if + /// you are dereferencing the `GuestPtr`s while holding a reference to another slice via + /// `GuestPtr::as_raw`. Not required if using `GuestPtr::as_raw` on this pointer. + pub fn borrow_slice<'a, T>(&mut self, p: &GuestPtr<'a, [T]>) -> Result<(), GuestError> + where + T: GuestType<'a>, + { + let (start, elems) = p.offset(); + let len = T::guest_size() + .checked_mul(elems) + .ok_or_else(|| GuestError::PtrOverflow)?; + self.borrow(Region { start, len }) + } + + /// Borrow the slice of memory pointed to by a `GuestPtr`. This is required for safety if + /// you are dereferencing the `GuestPtr`s while holding a reference to another slice via + /// `GuestPtr::as_raw`. Not required if using `GuestPtr::as_raw` on this pointer. + pub fn borrow_str(&mut self, p: &GuestPtr) -> Result<(), GuestError> { + let (start, len) = p.offset(); + self.borrow(Region { start, len }) + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn nonoverlapping() { + let mut bs = GuestBorrows::new(); + let r1 = Region::new(0, 10); + let r2 = Region::new(10, 10); + assert!(!r1.overlaps(r2)); + bs.borrow(r1).expect("can borrow r1"); + bs.borrow(r2).expect("can borrow r2"); + + let mut bs = GuestBorrows::new(); + let r1 = Region::new(10, 10); + let r2 = Region::new(0, 10); + assert!(!r1.overlaps(r2)); + bs.borrow(r1).expect("can borrow r1"); + bs.borrow(r2).expect("can borrow r2"); + } + + #[test] + fn overlapping() { + let mut bs = GuestBorrows::new(); + let r1 = Region::new(0, 10); + let r2 = Region::new(9, 10); + assert!(r1.overlaps(r2)); + bs.borrow(r1).expect("can borrow r1"); + assert!(bs.borrow(r2).is_err(), "cant borrow r2"); + + let mut bs = GuestBorrows::new(); + let r1 = Region::new(0, 10); + let r2 = Region::new(2, 5); + assert!(r1.overlaps(r2)); + bs.borrow(r1).expect("can borrow r1"); + assert!(bs.borrow(r2).is_err(), "cant borrow r2"); + + let mut bs = GuestBorrows::new(); + let r1 = Region::new(9, 10); + let r2 = Region::new(0, 10); + assert!(r1.overlaps(r2)); + bs.borrow(r1).expect("can borrow r1"); + assert!(bs.borrow(r2).is_err(), "cant borrow r2"); + + let mut bs = GuestBorrows::new(); + let r1 = Region::new(2, 5); + let r2 = Region::new(0, 10); + assert!(r1.overlaps(r2)); + bs.borrow(r1).expect("can borrow r1"); + assert!(bs.borrow(r2).is_err(), "cant borrow r2"); + + let mut bs = GuestBorrows::new(); + let r1 = Region::new(2, 5); + let r2 = Region::new(10, 5); + let r3 = Region::new(15, 5); + let r4 = Region::new(0, 10); + assert!(r1.overlaps(r4)); + bs.borrow(r1).expect("can borrow r1"); + bs.borrow(r2).expect("can borrow r2"); + bs.borrow(r3).expect("can borrow r3"); + assert!(bs.borrow(r4).is_err(), "cant borrow r4"); + } +} diff --git a/crates/wiggle/crates/runtime/src/error.rs b/crates/wiggle/crates/runtime/src/error.rs new file mode 100644 index 0000000000..8a163dbb63 --- /dev/null +++ b/crates/wiggle/crates/runtime/src/error.rs @@ -0,0 +1,36 @@ +use crate::Region; +use thiserror::Error; + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum GuestError { + #[error("Invalid flag value {0}")] + InvalidFlagValue(&'static str), + #[error("Invalid enum value {0}")] + InvalidEnumValue(&'static str), + #[error("Pointer overflow")] + PtrOverflow, + #[error("Pointer out of bounds: {0:?}")] + PtrOutOfBounds(Region), + #[error("Pointer not aligned to {1}: {0:?}")] + PtrNotAligned(Region, u32), + #[error("Pointer already borrowed: {0:?}")] + PtrBorrowed(Region), + #[error("In func {funcname}:{location}:")] + InFunc { + funcname: &'static str, + location: &'static str, + #[source] + err: Box, + }, + #[error("In data {typename}.{field}:")] + InDataField { + typename: String, + field: String, + #[source] + err: Box, + }, + #[error("Invalid UTF-8 encountered: {0:?}")] + InvalidUtf8(#[from] ::std::str::Utf8Error), + #[error("Int conversion error: {0:?}")] + TryFromIntError(#[from] ::std::num::TryFromIntError), +} diff --git a/crates/wiggle/crates/runtime/src/guest_type.rs b/crates/wiggle/crates/runtime/src/guest_type.rs new file mode 100644 index 0000000000..1a654da17e --- /dev/null +++ b/crates/wiggle/crates/runtime/src/guest_type.rs @@ -0,0 +1,136 @@ +use crate::{GuestError, GuestPtr}; +use std::mem; + +pub trait GuestErrorType<'a> { + type Context; + fn success() -> Self; + fn from_error(e: GuestError, ctx: &Self::Context) -> Self; +} + +/// A trait for types that are intended to be pointees in `GuestPtr`. +/// +/// This trait abstracts how to read/write information from the guest memory, as +/// well as how to offset elements in an array of guest memory. This layer of +/// abstraction allows the guest representation of a type to be different from +/// the host representation of a type, if necessary. It also allows for +/// validation when reading/writing. +pub trait GuestType<'a>: Sized { + /// Returns the size, in bytes, of this type in the guest memory. + fn guest_size() -> u32; + + /// Returns the required alignment of this type, in bytes, for both guest + /// and host memory. + fn guest_align() -> usize; + + /// Reads this value from the provided `ptr`. + /// + /// Must internally perform any safety checks necessary and is allowed to + /// fail if the bytes pointed to are also invalid. + /// + /// Typically if you're implementing this by hand you'll want to delegate to + /// other safe implementations of this trait (e.g. for primitive types like + /// `u32`) rather than writing lots of raw code yourself. + fn read(ptr: &GuestPtr<'a, Self>) -> Result; + + /// Writes a value to `ptr` after verifying that `ptr` is indeed valid to + /// store `val`. + /// + /// Similar to `read`, you'll probably want to implement this in terms of + /// other primitives. + fn write(ptr: &GuestPtr<'_, Self>, val: Self) -> Result<(), GuestError>; +} + +/// A trait for `GuestType`s that have the same representation in guest memory +/// as in Rust. These types can be used with the `GuestPtr::as_raw` method to +/// view as a slice. +/// +/// Unsafe trait because a correct GuestTypeTransparent implemengation ensures that the +/// GuestPtr::as_raw methods are safe. This trait should only ever be implemented +/// by wiggle_generate-produced code. +pub unsafe trait GuestTypeTransparent<'a>: GuestType<'a> { + /// Checks that the memory at `ptr` is a valid representation of `Self`. + /// + /// Assumes that memory safety checks have already been performed: `ptr` + /// has been checked to be aligned correctly and reside in memory using + /// `GuestMemory::validate_size_align` + fn validate(ptr: *mut Self) -> Result<(), GuestError>; +} + +macro_rules! primitives { + ($($i:ident)*) => ($( + impl<'a> GuestType<'a> for $i { + fn guest_size() -> u32 { mem::size_of::() as u32 } + fn guest_align() -> usize { mem::align_of::() } + + #[inline] + fn read(ptr: &GuestPtr<'a, Self>) -> Result { + // Any bit pattern for any primitive implemented with this + // macro is safe, so our `validate_size_align` method will + // guarantee that if we are given a pointer it's valid for the + // size of our type as well as properly aligned. Consequently we + // should be able to safely ready the pointer just after we + // validated it, returning it along here. + let host_ptr = ptr.mem().validate_size_align( + ptr.offset(), + Self::guest_align(), + Self::guest_size(), + )?; + Ok(unsafe { *host_ptr.cast::() }) + } + + #[inline] + fn write(ptr: &GuestPtr<'_, Self>, val: Self) -> Result<(), GuestError> { + let host_ptr = ptr.mem().validate_size_align( + ptr.offset(), + Self::guest_align(), + Self::guest_size(), + )?; + // Similar to above `as_raw` will do a lot of validation, and + // then afterwards we can safely write our value into the + // memory location. + unsafe { + *host_ptr.cast::() = val; + } + Ok(()) + } + } + + unsafe impl<'a> GuestTypeTransparent<'a> for $i { + #[inline] + fn validate(_ptr: *mut $i) -> Result<(), GuestError> { + // All bit patterns are safe, nothing to do here + Ok(()) + } + } + + )*) +} + +primitives! { + // signed + i8 i16 i32 i64 i128 isize + // unsigned + u8 u16 u32 u64 u128 usize + // floats + f32 f64 +} + +// Support pointers-to-pointers where pointers are always 32-bits in wasm land +impl<'a, T> GuestType<'a> for GuestPtr<'a, T> { + fn guest_size() -> u32 { + u32::guest_size() + } + + fn guest_align() -> usize { + u32::guest_align() + } + + fn read(ptr: &GuestPtr<'a, Self>) -> Result { + let offset = ptr.cast::().read()?; + Ok(GuestPtr::new(ptr.mem(), offset)) + } + + fn write(ptr: &GuestPtr<'_, Self>, val: Self) -> Result<(), GuestError> { + ptr.cast::().write(val.offset()) + } +} diff --git a/crates/wiggle/crates/runtime/src/lib.rs b/crates/wiggle/crates/runtime/src/lib.rs new file mode 100644 index 0000000000..a7f890ca56 --- /dev/null +++ b/crates/wiggle/crates/runtime/src/lib.rs @@ -0,0 +1,547 @@ +use std::cell::Cell; +use std::fmt; +use std::marker; +use std::rc::Rc; +use std::slice; +use std::str; +use std::sync::Arc; + +mod borrow; +mod error; +mod guest_type; +mod region; + +pub use borrow::GuestBorrows; +pub use error::GuestError; +pub use guest_type::{GuestErrorType, GuestType, GuestTypeTransparent}; +pub use region::Region; + +/// A trait which abstracts how to get at the region of host memory taht +/// contains guest memory. +/// +/// All `GuestPtr` types will contain a handle to this trait, signifying where +/// the pointer is actually pointing into. This type will need to be implemented +/// for the host's memory storage object. +/// +/// # Safety +/// +/// Safety around this type is tricky, and the trait is `unsafe` since there are +/// a few contracts you need to uphold to implement this type correctly and have +/// everything else in this crate work out safely. +/// +/// The most important method of this trait is the `base` method. This returns, +/// in host memory, a pointer and a length. The pointer should point to valid +/// memory for the guest to read/write for the length contiguous bytes +/// afterwards. +/// +/// The region returned by `base` must not only be valid, however, but it must +/// be valid for "a period of time before the guest is reentered". This isn't +/// exactly well defined but the general idea is that `GuestMemory` is allowed +/// to change under our feet to accomodate instructions like `memory.grow` or +/// other guest modifications. Memory, however, cannot be changed if the guest +/// is not reentered or if no explicitly action is taken to modify the guest +/// memory. +/// +/// This provides the guarantee that host pointers based on the return value of +/// `base` have a dynamic period for which they are valid. This time duration +/// must be "somehow nonzero in length" to allow users of `GuestMemory` and +/// `GuestPtr` to safely read and write interior data. +/// +/// # Using Raw Pointers +/// +/// Methods like [`GuestMemory::base`] or [`GuestPtr::as_raw`] will return raw +/// pointers to use. Returning raw pointers is significant because it shows +/// there are hazards with using the returned pointers, and they can't blanket +/// be used in a safe fashion. It is possible to use these pointers safely, but +/// any usage needs to uphold a few guarantees. +/// +/// * Whenever a `*mut T` is accessed or modified, it must be guaranteed that +/// since the pointer was originally obtained the guest memory wasn't +/// relocated in any way. This means you can't call back into the guest, call +/// other arbitrary functions which might call into the guest, etc. The +/// problem here is that the guest could execute instructions like +/// `memory.grow` which would invalidate the raw pointer. If, however, after +/// you acquire `*mut T` you only execute your own code and it doesn't touch +/// the guest, then `*mut T` is still guaranteed to point to valid code. +/// +/// * Furthermore, Rust's aliasing rules must still be upheld. For example you +/// can't have two `&mut T` types that point to the area or overlap in any +/// way. This in particular becomes an issue when you're dealing with multiple +/// `GuestPtr` types. If you want to simultaneously work with them then you +/// need to dynamically validate that you're either working with them all in a +/// shared fashion (e.g. as if they were `&T`) or you must verify that they do +/// not overlap to work with them as `&mut T`. +/// +/// Note that safely using the raw pointers is relatively difficult. This crate +/// strives to provide utilities to safely work with guest pointers so long as +/// the previous guarantees are all upheld. If advanced operations are done with +/// guest pointers it's recommended to be extremely cautious and thoroughly +/// consider possible ramifications with respect to this API before codifying +/// implementation details. +pub unsafe trait GuestMemory { + /// Returns the base allocation of this guest memory, located in host + /// memory. + /// + /// A pointer/length pair are returned to signify where the guest memory + /// lives in the host, and how many contiguous bytes the memory is valid for + /// after the returned pointer. + /// + /// Note that there are safety guarantees about this method that + /// implementations must uphold, and for more details see the + /// [`GuestMemory`] documentation. + fn base(&self) -> (*mut u8, u32); + + /// Validates a guest-relative pointer given various attributes, and returns + /// the corresponding host pointer. + /// + /// * `offset` - this is the guest-relative pointer, an offset from the + /// base. + /// * `align` - this is the desired alignment of the guest pointer, and if + /// successful the host pointer will be guaranteed to have this alignment. + /// * `len` - this is the number of bytes, after `offset`, that the returned + /// pointer must be valid for. + /// + /// This function will guarantee that the returned pointer is in-bounds of + /// `base`, *at this time*, for `len` bytes and has alignment `align`. If + /// any guarantees are not upheld then an error will be returned. + /// + /// Note that the returned pointer is an unsafe pointer. This is not safe to + /// use in general because guest memory can be relocated. Additionally the + /// guest may be modifying/reading memory as well. Consult the + /// [`GuestMemory`] documentation for safety information about using this + /// returned pointer. + fn validate_size_align( + &self, + offset: u32, + align: usize, + len: u32, + ) -> Result<*mut u8, GuestError> { + let (base_ptr, base_len) = self.base(); + let region = Region { start: offset, len }; + + // Figure out our pointer to the start of memory + let start = match (base_ptr as usize).checked_add(offset as usize) { + Some(ptr) => ptr, + None => return Err(GuestError::PtrOverflow), + }; + // and use that to figure out the end pointer + let end = match start.checked_add(len as usize) { + Some(ptr) => ptr, + None => return Err(GuestError::PtrOverflow), + }; + // and then verify that our end doesn't reach past the end of our memory + if end > (base_ptr as usize) + (base_len as usize) { + return Err(GuestError::PtrOutOfBounds(region)); + } + // and finally verify that the alignment is correct + if start % align != 0 { + return Err(GuestError::PtrNotAligned(region, align as u32)); + } + Ok(start as *mut u8) + } + + /// Convenience method for creating a `GuestPtr` at a particular offset. + /// + /// Note that `T` can be almost any type, and typically `offset` is a `u32`. + /// The exception is slices and strings, in which case `offset` is a `(u32, + /// u32)` of `(offset, length)`. + fn ptr<'a, T>(&'a self, offset: T::Pointer) -> GuestPtr<'a, T> + where + Self: Sized, + T: ?Sized + Pointee, + { + GuestPtr::new(self, offset) + } +} + +// Forwarding trait implementations to the original type + +unsafe impl<'a, T: ?Sized + GuestMemory> GuestMemory for &'a T { + fn base(&self) -> (*mut u8, u32) { + T::base(self) + } +} + +unsafe impl<'a, T: ?Sized + GuestMemory> GuestMemory for &'a mut T { + fn base(&self) -> (*mut u8, u32) { + T::base(self) + } +} + +unsafe impl GuestMemory for Box { + fn base(&self) -> (*mut u8, u32) { + T::base(self) + } +} + +unsafe impl GuestMemory for Rc { + fn base(&self) -> (*mut u8, u32) { + T::base(self) + } +} + +unsafe impl GuestMemory for Arc { + fn base(&self) -> (*mut u8, u32) { + T::base(self) + } +} + +/// A *guest* pointer into host memory. +/// +/// This type represents a pointer from the guest that points into host memory. +/// Internally a `GuestPtr` contains a handle to its original [`GuestMemory`] as +/// well as the offset into the memory that the pointer is pointing at. +/// +/// Presence of a [`GuestPtr`] does not imply any form of validity. Pointers can +/// be out-of-bounds, misaligned, etc. It is safe to construct a `GuestPtr` with +/// any offset at any time. Consider a `GuestPtr` roughly equivalent to `*mut +/// T`, although there are a few more safety guarantees around this type. +/// +/// ## Slices and Strings +/// +/// Note that the type parameter does not need to implement the `Sized` trait, +/// so you can implement types such as this: +/// +/// * `GuestPtr<'_, str>` - a pointer to a guest string +/// * `GuestPtr<'_, [T]>` - a pointer to a guest array +/// +/// Unsized types such as this may have extra methods and won't have methods +/// like [`GuestPtr::read`] or [`GuestPtr::write`]. +/// +/// ## Type parameter and pointee +/// +/// The `T` type parameter is largely intended for more static safety in Rust as +/// well as having a better handle on what we're pointing to. A `GuestPtr`, +/// however, does not necessarily literally imply a guest pointer pointing to +/// type `T`. Instead the [`GuestType`] trait is a layer of abstraction where +/// `GuestPtr` may actually be a pointer to `U` in guest memory, but you can +/// construct a `T` from a `U`. +/// +/// For example `GuestPtr>` is a valid type, but this is actually +/// more equivalent to `GuestPtr` because guest pointers are always +/// 32-bits. That being said you can create a `GuestPtr` from a `u32`. +/// +/// Additionally `GuestPtr` will actually delegate, typically, to and +/// implementation which loads the underlying data as `GuestPtr` (or +/// similar) and then the bytes loaded are validated to fit within the +/// definition of `MyEnum` before `MyEnum` is returned. +/// +/// For more information see the [`GuestPtr::read`] and [`GuestPtr::write`] +/// methods. In general though be extremely careful about writing `unsafe` code +/// when working with a `GuestPtr` if you're not using one of the +/// already-attached helper methods. +pub struct GuestPtr<'a, T: ?Sized + Pointee> { + mem: &'a (dyn GuestMemory + 'a), + pointer: T::Pointer, + _marker: marker::PhantomData<&'a Cell>, +} + +impl<'a, T: ?Sized + Pointee> GuestPtr<'a, T> { + /// Creates a new `GuestPtr` from the given `mem` and `pointer` values. + /// + /// Note that for sized types like `u32`, `GuestPtr`, etc, the `pointer` + /// vlue is a `u32` offset into guest memory. For slices and strings, + /// `pointer` is a `(u32, u32)` offset/length pair. + pub fn new(mem: &'a (dyn GuestMemory + 'a), pointer: T::Pointer) -> GuestPtr<'_, T> { + GuestPtr { + mem, + pointer, + _marker: marker::PhantomData, + } + } + + /// Returns the offset of this pointer in guest memory. + /// + /// Note that for sized types this returns a `u32`, but for slices and + /// strings it returns a `(u32, u32)` pointer/length pair. + pub fn offset(&self) -> T::Pointer { + self.pointer + } + + /// Returns the guest memory that this pointer is coming from. + pub fn mem(&self) -> &'a (dyn GuestMemory + 'a) { + self.mem + } + + /// Casts this `GuestPtr` type to a different type. + /// + /// This is a safe method which is useful for simply reinterpreting the type + /// parameter on this `GuestPtr`. Note that this is a safe method, where + /// again there's no guarantees about alignment, validity, in-bounds-ness, + /// etc of the returned pointer. + pub fn cast(&self) -> GuestPtr<'a, U> + where + T: Pointee, + { + GuestPtr::new(self.mem, self.pointer) + } + + /// Safely read a value from this pointer. + /// + /// This is a fun method, and is one of the lynchpins of this + /// implementation. The highlight here is that this is a *safe* operation, + /// not an unsafe one like `*mut T`. This works for a few reasons: + /// + /// * The `unsafe` contract of the `GuestMemory` trait means that there's + /// always at least some backing memory for this `GuestPtr`. + /// + /// * This does not use Rust-intrinsics to read the type `T`, but rather it + /// delegates to `T`'s implementation of [`GuestType`] to actually read + /// the underlying data. This again is a safe method, so any unsafety, if + /// any, must be internally documented. + /// + /// * Eventually what typically happens it that this bottoms out in the read + /// implementations for primitives types (like `i32`) which can safely be + /// read at any time, and then it's up to the runtime to determine what to + /// do with the bytes it read in a safe manner. + /// + /// Naturally lots of things can still go wrong, such as out-of-bounds + /// checks, alignment checks, validity checks (e.g. for enums), etc. All of + /// these check failures, however, are returned as a [`GuestError`] in the + /// `Result` here, and `Ok` is only returned if all the checks passed. + pub fn read(&self) -> Result + where + T: GuestType<'a>, + { + T::read(self) + } + + /// Safely write a value to this pointer. + /// + /// This method, like [`GuestPtr::read`], is pretty crucial for the safe + /// operation of this crate. All the same reasons apply though for why this + /// method is safe, even eventually bottoming out in primitives like writing + /// an `i32` which is safe to write bit patterns into memory at any time due + /// to the guarantees of [`GuestMemory`]. + /// + /// Like `read`, `write` can fail due to any manner of pointer checks, but + /// any failure is returned as a [`GuestError`]. + pub fn write(&self, val: T) -> Result<(), GuestError> + where + T: GuestType<'a>, + { + T::write(self, val) + } + + /// Performs pointer arithmetic on this pointer, moving the pointer forward + /// `amt` slots. + /// + /// This will either return the resulting pointer or `Err` if the pointer + /// arithmetic calculation would overflow around the end of the address + /// space. + pub fn add(&self, amt: u32) -> Result, GuestError> + where + T: GuestType<'a> + Pointee, + { + let offset = amt + .checked_mul(T::guest_size()) + .and_then(|o| self.pointer.checked_add(o)); + let offset = match offset { + Some(o) => o, + None => return Err(GuestError::PtrOverflow), + }; + Ok(GuestPtr::new(self.mem, offset)) + } + + /// Returns a `GuestPtr` for an array of `T`s using this pointer as the + /// base. + pub fn as_array(&self, elems: u32) -> GuestPtr<'a, [T]> + where + T: GuestType<'a> + Pointee, + { + GuestPtr::new(self.mem, (self.pointer, elems)) + } +} + +impl<'a, T> GuestPtr<'a, [T]> { + /// For slices, specifically returns the relative pointer to the base of the + /// array. + /// + /// This is similar to `<[T]>::as_ptr()` + pub fn offset_base(&self) -> u32 { + self.pointer.0 + } + + /// For slices, returns the length of the slice, in units. + pub fn len(&self) -> u32 { + self.pointer.1 + } + + /// Returns an iterator over interior pointers. + /// + /// Each item is a `Result` indicating whether it overflowed past the end of + /// the address space or not. + pub fn iter<'b>( + &'b self, + ) -> impl ExactSizeIterator, GuestError>> + 'b + where + T: GuestType<'a>, + { + let base = self.as_ptr(); + (0..self.len()).map(move |i| base.add(i)) + } + + /// Attempts to read a raw `*mut [T]` pointer from this pointer, performing + /// bounds checks and type validation. + /// The resulting `*mut [T]` can be used as a `&mut [t]` as long as the + /// reference is dropped before any Wasm code is re-entered. + /// + /// This function will return a raw pointer into host memory if all checks + /// succeed (valid utf-8, valid pointers, etc). If any checks fail then + /// `GuestError` will be returned. + /// + /// Note that the `*mut [T]` pointer is still unsafe to use in general, but + /// there are specific situations that it is safe to use. For more + /// information about using the raw pointer, consult the [`GuestMemory`] + /// trait documentation. + /// + /// For safety against overlapping mutable borrows, the user must use the + /// same `GuestBorrows` to create all *mut str or *mut [T] that are alive + /// at the same time. + pub fn as_raw(&self, bc: &mut GuestBorrows) -> Result<*mut [T], GuestError> + where + T: GuestTypeTransparent<'a>, + { + let len = match self.pointer.1.checked_mul(T::guest_size()) { + Some(l) => l, + None => return Err(GuestError::PtrOverflow), + }; + let ptr = + self.mem + .validate_size_align(self.pointer.0, T::guest_align(), len)? as *mut T; + + bc.borrow(Region { + start: self.pointer.0, + len, + })?; + + // Validate all elements in slice. + // SAFETY: ptr has been validated by self.mem.validate_size_align + for offs in 0..self.pointer.1 { + T::validate(unsafe { ptr.add(offs as usize) })?; + } + + // SAFETY: iff there are no overlapping borrows (all uses of as_raw use this same + // GuestBorrows), its valid to construct a *mut [T] + unsafe { + let s = slice::from_raw_parts_mut(ptr, self.pointer.1 as usize); + Ok(s as *mut [T]) + } + } + + /// Returns a `GuestPtr` pointing to the base of the array for the interior + /// type `T`. + pub fn as_ptr(&self) -> GuestPtr<'a, T> { + GuestPtr::new(self.mem, self.offset_base()) + } +} + +impl<'a> GuestPtr<'a, str> { + /// For strings, returns the relative pointer to the base of the string + /// allocation. + pub fn offset_base(&self) -> u32 { + self.pointer.0 + } + + /// Returns the length, in bytes, of th estring. + pub fn len(&self) -> u32 { + self.pointer.1 + } + + /// Returns a raw pointer for the underlying slice of bytes that this + /// pointer points to. + pub fn as_bytes(&self) -> GuestPtr<'a, [u8]> { + GuestPtr::new(self.mem, self.pointer) + } + + /// Attempts to read a raw `*mut str` pointer from this pointer, performing + /// bounds checks and utf-8 checks. + /// The resulting `*mut str` can be used as a `&mut str` as long as the + /// reference is dropped before any Wasm code is re-entered. + /// + /// This function will return a raw pointer into host memory if all checks + /// succeed (valid utf-8, valid pointers, etc). If any checks fail then + /// `GuestError` will be returned. + /// + /// Note that the `*mut str` pointer is still unsafe to use in general, but + /// there are specific situations that it is safe to use. For more + /// information about using the raw pointer, consult the [`GuestMemory`] + /// trait documentation. + /// + /// For safety against overlapping mutable borrows, the user must use the + /// same `GuestBorrows` to create all *mut str or *mut [T] that are alive + /// at the same time. + pub fn as_raw(&self, bc: &mut GuestBorrows) -> Result<*mut str, GuestError> { + let ptr = self + .mem + .validate_size_align(self.pointer.0, 1, self.pointer.1)?; + + bc.borrow(Region { + start: self.pointer.0, + len: self.pointer.1, + })?; + + // SAFETY: iff there are no overlapping borrows (all uses of as_raw use this same + // GuestBorrows), its valid to construct a *mut str + unsafe { + let s = slice::from_raw_parts_mut(ptr, self.pointer.1 as usize); + match str::from_utf8_mut(s) { + Ok(s) => Ok(s), + Err(e) => Err(GuestError::InvalidUtf8(e)), + } + } + } +} + +impl Clone for GuestPtr<'_, T> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for GuestPtr<'_, T> {} + +impl fmt::Debug for GuestPtr<'_, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + T::debug(self.pointer, f) + } +} + +mod private { + pub trait Sealed {} + impl Sealed for T {} + impl Sealed for [T] {} + impl Sealed for str {} +} + +/// Types that can be pointed to by `GuestPtr`. +/// +/// In essence everything can, and the only special-case is unsized types like +/// `str` and `[T]` which have special implementations. +pub trait Pointee: private::Sealed { + #[doc(hidden)] + type Pointer: Copy; + #[doc(hidden)] + fn debug(pointer: Self::Pointer, f: &mut fmt::Formatter) -> fmt::Result; +} + +impl Pointee for T { + type Pointer = u32; + fn debug(pointer: Self::Pointer, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "*guest {:#x}", pointer) + } +} + +impl Pointee for [T] { + type Pointer = (u32, u32); + fn debug(pointer: Self::Pointer, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "*guest {:#x}/{}", pointer.0, pointer.1) + } +} + +impl Pointee for str { + type Pointer = (u32, u32); + fn debug(pointer: Self::Pointer, f: &mut fmt::Formatter) -> fmt::Result { + <[u8]>::debug(pointer, f) + } +} diff --git a/crates/wiggle/crates/runtime/src/region.rs b/crates/wiggle/crates/runtime/src/region.rs new file mode 100644 index 0000000000..e1e6084dc0 --- /dev/null +++ b/crates/wiggle/crates/runtime/src/region.rs @@ -0,0 +1,71 @@ +/// Represents a contiguous region in memory. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Region { + pub start: u32, + pub len: u32, +} + +impl Region { + pub fn new(start: u32, len: u32) -> Self { + assert!(len > 0, "Region cannot have 0 length"); + Self { start, len } + } + + /// Checks if this `Region` overlaps with `rhs` `Region`. + pub fn overlaps(&self, rhs: Region) -> bool { + let self_start = self.start as u64; + let self_end = self_start + (self.len - 1) as u64; + + let rhs_start = rhs.start as u64; + let rhs_end = rhs_start + (rhs.len - 1) as u64; + + if self_start <= rhs_start { + self_end >= rhs_start + } else { + rhs_end >= self_start + } + } + + pub fn extend(&self, times: u32) -> Self { + let len = self.len * times; + Self { + start: self.start, + len, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn nonoverlapping() { + let r1 = Region::new(0, 10); + let r2 = Region::new(10, 10); + assert!(!r1.overlaps(r2)); + + let r1 = Region::new(10, 10); + let r2 = Region::new(0, 10); + assert!(!r1.overlaps(r2)); + } + + #[test] + fn overlapping() { + let r1 = Region::new(0, 10); + let r2 = Region::new(9, 10); + assert!(r1.overlaps(r2)); + + let r1 = Region::new(0, 10); + let r2 = Region::new(2, 5); + assert!(r1.overlaps(r2)); + + let r1 = Region::new(9, 10); + let r2 = Region::new(0, 10); + assert!(r1.overlaps(r2)); + + let r1 = Region::new(2, 5); + let r2 = Region::new(0, 10); + assert!(r1.overlaps(r2)); + } +} diff --git a/crates/wiggle/crates/test/.gitignore b/crates/wiggle/crates/test/.gitignore new file mode 100644 index 0000000000..a9d37c560c --- /dev/null +++ b/crates/wiggle/crates/test/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/crates/wiggle/crates/test/Cargo.toml b/crates/wiggle/crates/test/Cargo.toml new file mode 100644 index 0000000000..88c599e46c --- /dev/null +++ b/crates/wiggle/crates/test/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "wiggle-test" +version = "0.1.0" +authors = ["Pat Hickey ", "Jakub Konka "] +edition = "2018" + +[dependencies] +wiggle-runtime = { path = "../runtime" } +proptest = "0.9" diff --git a/crates/wiggle/crates/test/src/lib.rs b/crates/wiggle/crates/test/src/lib.rs new file mode 100644 index 0000000000..8ec8f916dd --- /dev/null +++ b/crates/wiggle/crates/test/src/lib.rs @@ -0,0 +1,329 @@ +use proptest::prelude::*; +use std::cell::UnsafeCell; +use std::marker; +use wiggle_runtime::GuestMemory; + +#[derive(Debug, Clone)] +pub struct MemAreas(Vec); +impl MemAreas { + pub fn new() -> Self { + MemAreas(Vec::new()) + } + pub fn insert(&mut self, a: MemArea) { + // Find if `a` is already in the vector + match self.0.binary_search(&a) { + // It is present - insert it next to existing one + Ok(loc) => self.0.insert(loc, a), + // It is not present - heres where to insert it + Err(loc) => self.0.insert(loc, a), + } + } + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } +} + +impl From for MemAreas +where + R: AsRef<[MemArea]>, +{ + fn from(ms: R) -> MemAreas { + let mut out = MemAreas::new(); + for m in ms.as_ref().into_iter() { + out.insert(*m); + } + out + } +} + +impl Into> for MemAreas { + fn into(self) -> Vec { + self.0.clone() + } +} + +#[repr(align(4096))] +pub struct HostMemory { + buffer: UnsafeCell<[u8; 4096]>, +} +impl HostMemory { + pub fn new() -> Self { + HostMemory { + buffer: UnsafeCell::new([0; 4096]), + } + } + + pub fn mem_area_strat(align: u32) -> BoxedStrategy { + prop::num::u32::ANY + .prop_filter_map("needs to fit in memory", move |p| { + let p_aligned = p - (p % align); // Align according to argument + let ptr = p_aligned % 4096; // Put inside memory + if ptr + align < 4096 { + Some(MemArea { ptr, len: align }) + } else { + None + } + }) + .boxed() + } + + /// Takes a sorted list or memareas, and gives a sorted list of memareas covering + /// the parts of memory not covered by the previous + pub fn invert(regions: &MemAreas) -> MemAreas { + let mut out = MemAreas::new(); + let mut start = 0; + for r in regions.iter() { + let len = r.ptr - start; + if len > 0 { + out.insert(MemArea { + ptr: start, + len: r.ptr - start, + }); + } + start = r.ptr + r.len; + } + if start < 4096 { + out.insert(MemArea { + ptr: start, + len: 4096 - start, + }); + } + out + } + + pub fn byte_slice_strat(size: u32, exclude: &MemAreas) -> BoxedStrategy { + let available: Vec = Self::invert(exclude) + .iter() + .flat_map(|a| a.inside(size)) + .collect(); + + Just(available) + .prop_filter("available memory for allocation", |a| !a.is_empty()) + .prop_flat_map(|a| prop::sample::select(a)) + .boxed() + } +} + +unsafe impl GuestMemory for HostMemory { + fn base(&self) -> (*mut u8, u32) { + unsafe { + let ptr = self.buffer.get(); + ((*ptr).as_mut_ptr(), (*ptr).len() as u32) + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct MemArea { + pub ptr: u32, + pub len: u32, +} + +impl MemArea { + // This code is a whole lot like the Region::overlaps func thats at the core of the code under + // test. + // So, I implemented this one with std::ops::Range so it is less likely I wrote the same bug in two + // places. + pub fn overlapping(&self, b: Self) -> bool { + // a_range is all elems in A + let a_range = std::ops::Range { + start: self.ptr, + end: self.ptr + self.len, // std::ops::Range is open from the right + }; + // b_range is all elems in B + let b_range = std::ops::Range { + start: b.ptr, + end: b.ptr + b.len, + }; + // No element in B is contained in A: + for b_elem in b_range.clone() { + if a_range.contains(&b_elem) { + return true; + } + } + // No element in A is contained in B: + for a_elem in a_range { + if b_range.contains(&a_elem) { + return true; + } + } + return false; + } + pub fn non_overlapping_set(areas: M) -> bool + where + M: Into, + { + let areas = areas.into(); + for (aix, a) in areas.iter().enumerate() { + for (bix, b) in areas.iter().enumerate() { + if aix != bix { + // (A, B) is every pairing of areas + if a.overlapping(*b) { + return false; + } + } + } + } + return true; + } + + /// Enumerate all memareas of size `len` inside a given area + fn inside(&self, len: u32) -> impl Iterator { + let end: i64 = self.len as i64 - len as i64; + let start = self.ptr; + (0..end).into_iter().map(move |v| MemArea { + ptr: start + v as u32, + len, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn hostmemory_is_aligned() { + let h = HostMemory::new(); + assert_eq!(h.base().0 as usize % 4096, 0); + let h = Box::new(h); + assert_eq!(h.base().0 as usize % 4096, 0); + } + + #[test] + fn invert() { + fn invert_equality(input: &[MemArea], expected: &[MemArea]) { + let input: MemAreas = input.into(); + let inverted: Vec = HostMemory::invert(&input).into(); + assert_eq!(expected, inverted.as_slice()); + } + + invert_equality(&[], &[MemArea { ptr: 0, len: 4096 }]); + invert_equality( + &[MemArea { ptr: 0, len: 1 }], + &[MemArea { ptr: 1, len: 4095 }], + ); + + invert_equality( + &[MemArea { ptr: 1, len: 1 }], + &[MemArea { ptr: 0, len: 1 }, MemArea { ptr: 2, len: 4094 }], + ); + + invert_equality( + &[MemArea { ptr: 1, len: 4095 }], + &[MemArea { ptr: 0, len: 1 }], + ); + + invert_equality( + &[MemArea { ptr: 0, len: 1 }, MemArea { ptr: 1, len: 4095 }], + &[], + ); + + invert_equality( + &[MemArea { ptr: 1, len: 2 }, MemArea { ptr: 4, len: 1 }], + &[ + MemArea { ptr: 0, len: 1 }, + MemArea { ptr: 3, len: 1 }, + MemArea { ptr: 5, len: 4091 }, + ], + ); + } + + fn set_of_slices_strat( + s1: u32, + s2: u32, + s3: u32, + ) -> BoxedStrategy<(MemArea, MemArea, MemArea)> { + HostMemory::byte_slice_strat(s1, &MemAreas::new()) + .prop_flat_map(move |a1| { + ( + Just(a1), + HostMemory::byte_slice_strat(s2, &MemAreas::from(&[a1])), + ) + }) + .prop_flat_map(move |(a1, a2)| { + ( + Just(a1), + Just(a2), + HostMemory::byte_slice_strat(s3, &MemAreas::from(&[a1, a2])), + ) + }) + .boxed() + } + + #[test] + fn trivial_inside() { + let a = MemArea { ptr: 24, len: 4072 }; + let interior = a.inside(24).collect::>(); + + assert!(interior.len() > 0); + } + + proptest! { + #[test] + // For some random region of decent size + fn inside(r in HostMemory::mem_area_strat(123)) { + let set_of_r = MemAreas::from(&[r]); + // All regions outside of r: + let exterior = HostMemory::invert(&set_of_r); + // All regions inside of r: + let interior = r.inside(22); + for i in interior { + // i overlaps with r: + assert!(r.overlapping(i)); + // i is inside r: + assert!(i.ptr >= r.ptr); + assert!(r.ptr + r.len >= i.ptr + i.len); + // the set of exterior and i is non-overlapping + let mut all = exterior.clone(); + all.insert(i); + assert!(MemArea::non_overlapping_set(all)); + } + } + + #[test] + fn byte_slices((s1, s2, s3) in set_of_slices_strat(12, 34, 56)) { + let all = MemAreas::from(&[s1, s2, s3]); + assert!(MemArea::non_overlapping_set(all)); + } + } +} + +use std::cell::RefCell; +use wiggle_runtime::GuestError; + +// In lucet, our Ctx struct needs a lifetime, so we're using one +// on the test as well. +pub struct WasiCtx<'a> { + pub guest_errors: RefCell>, + lifetime: marker::PhantomData<&'a ()>, +} + +impl<'a> WasiCtx<'a> { + pub fn new() -> Self { + Self { + guest_errors: RefCell::new(vec![]), + lifetime: marker::PhantomData, + } + } +} + +// Errno is used as a first return value in the functions above, therefore +// it must implement GuestErrorType with type Context = WasiCtx. +// The context type should let you do logging or debugging or whatever you need +// with these errors. We just push them to vecs. +#[macro_export] +macro_rules! impl_errno { + ( $errno:ty ) => { + impl<'a> wiggle_runtime::GuestErrorType<'a> for $errno { + type Context = WasiCtx<'a>; + fn success() -> $errno { + <$errno>::Ok + } + fn from_error(e: GuestError, ctx: &WasiCtx) -> $errno { + eprintln!("GUEST ERROR: {:?}", e); + ctx.guest_errors.borrow_mut().push(e); + types::Errno::InvalidArg + } + } + }; +} diff --git a/crates/wiggle/src/lib.rs b/crates/wiggle/src/lib.rs new file mode 100644 index 0000000000..84b96f1895 --- /dev/null +++ b/crates/wiggle/src/lib.rs @@ -0,0 +1,11 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use syn::parse_macro_input; + +#[proc_macro] +pub fn from_witx(args: TokenStream) -> TokenStream { + let config = parse_macro_input!(args as wiggle_generate::Config); + let doc = witx::load(&config.witx.paths).expect("loading witx"); + TokenStream::from(wiggle_generate::generate(&doc, &config)) +} diff --git a/crates/wiggle/tests/arrays.rs b/crates/wiggle/tests/arrays.rs new file mode 100644 index 0000000000..b0dea82a54 --- /dev/null +++ b/crates/wiggle/tests/arrays.rs @@ -0,0 +1,210 @@ +use proptest::prelude::*; +use wiggle_runtime::{GuestError, GuestMemory, GuestPtr}; +use wiggle_test::{impl_errno, HostMemory, MemArea, WasiCtx}; + +wiggle::from_witx!({ + witx: ["tests/arrays.witx"], + ctx: WasiCtx, +}); + +impl_errno!(types::Errno); + +impl<'a> arrays::Arrays for WasiCtx<'a> { + fn reduce_excuses( + &self, + excuses: &types::ConstExcuseArray, + ) -> Result { + let last = &excuses + .iter() + .last() + .expect("input array is non-empty") + .expect("valid ptr to ptr") + .read() + .expect("valid ptr to some Excuse value"); + Ok(last.read().expect("dereferencing ptr should succeed")) + } + + fn populate_excuses(&self, excuses: &types::ExcuseArray) -> Result<(), types::Errno> { + for excuse in excuses.iter() { + let ptr_to_excuse = excuse + .expect("valid ptr to ptr") + .read() + .expect("valid ptr to some Excuse value"); + ptr_to_excuse + .write(types::Excuse::Sleeping) + .expect("dereferencing mut ptr should succeed"); + } + Ok(()) + } +} + +#[derive(Debug)] +struct ReduceExcusesExcercise { + excuse_values: Vec, + excuse_ptr_locs: Vec, + array_ptr_loc: MemArea, + return_ptr_loc: MemArea, +} + +impl ReduceExcusesExcercise { + pub fn strat() -> BoxedStrategy { + (1..256u32) + .prop_flat_map(|len| { + let len_usize = len as usize; + ( + proptest::collection::vec(excuse_strat(), len_usize..=len_usize), + proptest::collection::vec(HostMemory::mem_area_strat(4), len_usize..=len_usize), + HostMemory::mem_area_strat(4 * len), + HostMemory::mem_area_strat(4), + ) + }) + .prop_map( + |(excuse_values, excuse_ptr_locs, array_ptr_loc, return_ptr_loc)| Self { + excuse_values, + excuse_ptr_locs, + array_ptr_loc, + return_ptr_loc, + }, + ) + .prop_filter("non-overlapping pointers", |e| { + let mut all = vec![e.array_ptr_loc, e.return_ptr_loc]; + all.extend(e.excuse_ptr_locs.iter()); + MemArea::non_overlapping_set(all) + }) + .boxed() + } + + pub fn test(&self) { + let mut ctx = WasiCtx::new(); + let mut host_memory = HostMemory::new(); + + // Populate memory with pointers to generated Excuse values + for (&excuse, ptr) in self.excuse_values.iter().zip(self.excuse_ptr_locs.iter()) { + host_memory + .ptr(ptr.ptr) + .write(excuse) + .expect("deref ptr mut to Excuse value"); + } + + // Populate the array with pointers to generated Excuse values + { + let array: GuestPtr<'_, [GuestPtr]> = + host_memory.ptr((self.array_ptr_loc.ptr, self.excuse_ptr_locs.len() as u32)); + for (slot, ptr) in array.iter().zip(&self.excuse_ptr_locs) { + let slot = slot.expect("array should be in bounds"); + slot.write(host_memory.ptr(ptr.ptr)) + .expect("should succeed in writing array"); + } + } + + let res = arrays::reduce_excuses( + &mut ctx, + &mut host_memory, + self.array_ptr_loc.ptr as i32, + self.excuse_ptr_locs.len() as i32, + self.return_ptr_loc.ptr as i32, + ); + + assert_eq!(res, types::Errno::Ok.into(), "reduce excuses errno"); + + let expected = *self + .excuse_values + .last() + .expect("generated vec of excuses should be non-empty"); + let given: types::Excuse = host_memory + .ptr(self.return_ptr_loc.ptr) + .read() + .expect("deref ptr to returned value"); + assert_eq!(expected, given, "reduce excuses return val"); + } +} +proptest! { + #[test] + fn reduce_excuses(e in ReduceExcusesExcercise::strat()) { + e.test() + } +} + +fn excuse_strat() -> impl Strategy { + prop_oneof![ + Just(types::Excuse::DogAte), + Just(types::Excuse::Traffic), + Just(types::Excuse::Sleeping), + ] + .boxed() +} + +#[derive(Debug)] +struct PopulateExcusesExcercise { + array_ptr_loc: MemArea, + elements: Vec, +} + +impl PopulateExcusesExcercise { + pub fn strat() -> BoxedStrategy { + (1..256u32) + .prop_flat_map(|len| { + let len_usize = len as usize; + ( + HostMemory::mem_area_strat(4 * len), + proptest::collection::vec(HostMemory::mem_area_strat(4), len_usize..=len_usize), + ) + }) + .prop_map(|(array_ptr_loc, elements)| Self { + array_ptr_loc, + elements, + }) + .prop_filter("non-overlapping pointers", |e| { + let mut all = vec![e.array_ptr_loc]; + all.extend(e.elements.iter()); + MemArea::non_overlapping_set(all) + }) + .boxed() + } + + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + // Populate array with valid pointers to Excuse type in memory + let ptr = host_memory.ptr::<[GuestPtr<'_, types::Excuse>]>(( + self.array_ptr_loc.ptr, + self.elements.len() as u32, + )); + for (ptr, val) in ptr.iter().zip(&self.elements) { + ptr.expect("should be valid pointer") + .write(host_memory.ptr(val.ptr)) + .expect("failed to write value"); + } + + let res = arrays::populate_excuses( + &ctx, + &host_memory, + self.array_ptr_loc.ptr as i32, + self.elements.len() as i32, + ); + assert_eq!(res, types::Errno::Ok.into(), "populate excuses errno"); + + let arr: GuestPtr<'_, [GuestPtr<'_, types::Excuse>]> = + host_memory.ptr((self.array_ptr_loc.ptr, self.elements.len() as u32)); + for el in arr.iter() { + let ptr_to_ptr = el + .expect("valid ptr to ptr") + .read() + .expect("valid ptr to some Excuse value"); + assert_eq!( + ptr_to_ptr + .read() + .expect("dereferencing ptr to some Excuse value"), + types::Excuse::Sleeping, + "element should equal Excuse::Sleeping" + ); + } + } +} +proptest! { + #[test] + fn populate_excuses(e in PopulateExcusesExcercise::strat()) { + e.test() + } +} diff --git a/crates/wiggle/tests/arrays.witx b/crates/wiggle/tests/arrays.witx new file mode 100644 index 0000000000..e8a81cee95 --- /dev/null +++ b/crates/wiggle/tests/arrays.witx @@ -0,0 +1,17 @@ +(use "errno.witx") +(use "excuse.witx") + +(typename $const_excuse_array (array (@witx const_pointer $excuse))) +(typename $excuse_array (array (@witx pointer $excuse))) + +(module $arrays + (@interface func (export "reduce_excuses") + (param $excuses $const_excuse_array) + (result $error $errno) + (result $reduced $excuse) + ) + (@interface func (export "populate_excuses") + (param $excuses $excuse_array) + (result $error $errno) + ) +) diff --git a/crates/wiggle/tests/atoms.rs b/crates/wiggle/tests/atoms.rs new file mode 100644 index 0000000000..d19d19c209 --- /dev/null +++ b/crates/wiggle/tests/atoms.rs @@ -0,0 +1,91 @@ +use proptest::prelude::*; +use wiggle_runtime::{GuestError, GuestMemory}; +use wiggle_test::{impl_errno, HostMemory, MemArea, WasiCtx}; + +wiggle::from_witx!({ + witx: ["tests/atoms.witx"], + ctx: WasiCtx, +}); + +impl_errno!(types::Errno); + +impl<'a> atoms::Atoms for WasiCtx<'a> { + fn int_float_args(&self, an_int: u32, an_float: f32) -> Result<(), types::Errno> { + println!("INT FLOAT ARGS: {} {}", an_int, an_float); + Ok(()) + } + fn double_int_return_float(&self, an_int: u32) -> Result { + Ok((an_int as f32) * 2.0) + } +} + +// There's nothing meaningful to test here - this just demonstrates the test machinery + +#[derive(Debug)] +struct IntFloatExercise { + pub an_int: u32, + pub an_float: f32, +} + +impl IntFloatExercise { + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + let e = atoms::int_float_args(&ctx, &host_memory, self.an_int as i32, self.an_float); + + assert_eq!(e, types::Errno::Ok.into(), "int_float_args error"); + } + + pub fn strat() -> BoxedStrategy { + (prop::num::u32::ANY, prop::num::f32::ANY) + .prop_map(|(an_int, an_float)| IntFloatExercise { an_int, an_float }) + .boxed() + } +} + +proptest! { + #[test] + fn int_float_exercise(e in IntFloatExercise::strat()) { + e.test() + } +} +#[derive(Debug)] +struct DoubleIntExercise { + pub input: u32, + pub return_loc: MemArea, +} + +impl DoubleIntExercise { + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + let e = atoms::double_int_return_float( + &ctx, + &host_memory, + self.input as i32, + self.return_loc.ptr as i32, + ); + + let return_val = host_memory + .ptr::(self.return_loc.ptr) + .read() + .expect("failed to read return"); + assert_eq!(e, types::Errno::Ok.into(), "errno"); + assert_eq!(return_val, (self.input as f32) * 2.0, "return val"); + } + + pub fn strat() -> BoxedStrategy { + (prop::num::u32::ANY, HostMemory::mem_area_strat(4)) + .prop_map(|(input, return_loc)| DoubleIntExercise { input, return_loc }) + .boxed() + } +} + +proptest! { + #[test] + fn double_int_return_float(e in DoubleIntExercise::strat()) { + e.test() + } +} diff --git a/crates/wiggle/tests/atoms.witx b/crates/wiggle/tests/atoms.witx new file mode 100644 index 0000000000..932d7c9ffd --- /dev/null +++ b/crates/wiggle/tests/atoms.witx @@ -0,0 +1,14 @@ +(use "errno.witx") + +(typename $alias_to_float f32) + +(module $atoms + (@interface func (export "int_float_args") + (param $an_int u32) + (param $an_float f32) + (result $error $errno)) + (@interface func (export "double_int_return_float") + (param $an_int u32) + (result $error $errno) + (result $doubled_it $alias_to_float)) +) diff --git a/crates/wiggle/tests/errno.witx b/crates/wiggle/tests/errno.witx new file mode 100644 index 0000000000..36ee67622e --- /dev/null +++ b/crates/wiggle/tests/errno.witx @@ -0,0 +1,13 @@ +(typename $errno + (enum u32 + ;;; Success + $ok + ;;; Invalid argument + $invalid_arg + ;;; I really don't want to + $dont_want_to + ;;; I am physically unable to + $physically_unable + ;;; Well, that's a picket line alright! + $picket_line)) + diff --git a/crates/wiggle/tests/excuse.witx b/crates/wiggle/tests/excuse.witx new file mode 100644 index 0000000000..14a927164e --- /dev/null +++ b/crates/wiggle/tests/excuse.witx @@ -0,0 +1,6 @@ +(typename $excuse + (enum u8 + $dog_ate + $traffic + $sleeping)) + diff --git a/crates/wiggle/tests/flags.rs b/crates/wiggle/tests/flags.rs new file mode 100644 index 0000000000..78939fc3d6 --- /dev/null +++ b/crates/wiggle/tests/flags.rs @@ -0,0 +1,101 @@ +use proptest::prelude::*; +use std::convert::TryFrom; +use wiggle_runtime::{GuestError, GuestMemory, GuestPtr}; +use wiggle_test::{impl_errno, HostMemory, MemArea, WasiCtx}; + +wiggle::from_witx!({ + witx: ["tests/flags.witx"], + ctx: WasiCtx, +}); + +impl_errno!(types::Errno); + +impl<'a> flags::Flags for WasiCtx<'a> { + fn configure_car( + &self, + old_config: types::CarConfig, + other_config_ptr: GuestPtr, + ) -> Result { + let other_config = other_config_ptr.read().map_err(|e| { + eprintln!("old_config_ptr error: {}", e); + types::Errno::InvalidArg + })?; + Ok(old_config ^ other_config) + } +} + +fn car_config_strat() -> impl Strategy { + (1u8..=types::CarConfig::ALL_FLAGS.into()) + .prop_map(|v| { + types::CarConfig::try_from(v).expect("invalid value for types::CarConfig flag") + }) + .boxed() +} + +#[derive(Debug)] +struct ConfigureCarExercise { + old_config: types::CarConfig, + other_config: types::CarConfig, + other_config_by_ptr: MemArea, + return_ptr_loc: MemArea, +} + +impl ConfigureCarExercise { + pub fn strat() -> BoxedStrategy { + ( + car_config_strat(), + car_config_strat(), + HostMemory::mem_area_strat(4), + HostMemory::mem_area_strat(4), + ) + .prop_map( + |(old_config, other_config, other_config_by_ptr, return_ptr_loc)| Self { + old_config, + other_config, + other_config_by_ptr, + return_ptr_loc, + }, + ) + .prop_filter("non-overlapping ptrs", |e| { + MemArea::non_overlapping_set(&[e.other_config_by_ptr, e.return_ptr_loc]) + }) + .boxed() + } + + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + // Populate input ptr + host_memory + .ptr(self.other_config_by_ptr.ptr) + .write(self.other_config) + .expect("deref ptr mut to CarConfig"); + + let res = flags::configure_car( + &ctx, + &host_memory, + self.old_config.into(), + self.other_config_by_ptr.ptr as i32, + self.return_ptr_loc.ptr as i32, + ); + assert_eq!(res, types::Errno::Ok.into(), "configure car errno"); + + let res_config = host_memory + .ptr::(self.return_ptr_loc.ptr) + .read() + .expect("deref to CarConfig value"); + + assert_eq!( + self.old_config ^ self.other_config, + res_config, + "returned CarConfig should be an XOR of inputs" + ); + } +} +proptest! { + #[test] + fn configure_car(e in ConfigureCarExercise::strat()) { + e.test() + } +} diff --git a/crates/wiggle/tests/flags.witx b/crates/wiggle/tests/flags.witx new file mode 100644 index 0000000000..b46f73d5b5 --- /dev/null +++ b/crates/wiggle/tests/flags.witx @@ -0,0 +1,16 @@ +(use "errno.witx") + +(typename $car_config + (flags u8 + $automatic + $awd + $suv)) + +(module $flags + (@interface func (export "configure_car") + (param $old_config $car_config) + (param $old_config_by_ptr (@witx const_pointer $car_config)) + (result $error $errno) + (result $new_config $car_config) + ) +) diff --git a/crates/wiggle/tests/handles.rs b/crates/wiggle/tests/handles.rs new file mode 100644 index 0000000000..c9e44122ec --- /dev/null +++ b/crates/wiggle/tests/handles.rs @@ -0,0 +1,74 @@ +use proptest::prelude::*; +use wiggle_runtime::{GuestError, GuestMemory, GuestType}; +use wiggle_test::{impl_errno, HostMemory, MemArea, WasiCtx}; + +const FD_VAL: u32 = 123; + +wiggle::from_witx!({ + witx: ["tests/handles.witx"], + ctx: WasiCtx, +}); + +impl_errno!(types::Errno); + +impl<'a> handle_examples::HandleExamples for WasiCtx<'a> { + fn fd_create(&self) -> Result { + Ok(types::Fd::from(FD_VAL)) + } + fn fd_consume(&self, fd: types::Fd) -> Result<(), types::Errno> { + println!("FD_CONSUME {}", fd); + if fd == types::Fd::from(FD_VAL) { + Ok(()) + } else { + Err(types::Errno::InvalidArg) + } + } +} + +#[derive(Debug)] +struct HandleExercise { + pub return_loc: MemArea, +} + +impl HandleExercise { + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + let e = handle_examples::fd_create(&ctx, &host_memory, self.return_loc.ptr as i32); + + assert_eq!(e, types::Errno::Ok.into(), "fd_create error"); + + let h_got: u32 = host_memory + .ptr(self.return_loc.ptr) + .read() + .expect("return ref_mut"); + + assert_eq!(h_got, 123, "fd_create return val"); + + let e = handle_examples::fd_consume(&ctx, &host_memory, h_got as i32); + + assert_eq!(e, types::Errno::Ok.into(), "fd_consume error"); + + let e = handle_examples::fd_consume(&ctx, &host_memory, h_got as i32 + 1); + + assert_eq!( + e, + types::Errno::InvalidArg.into(), + "fd_consume invalid error" + ); + } + + pub fn strat() -> BoxedStrategy { + (HostMemory::mem_area_strat(types::Fd::guest_size())) + .prop_map(|return_loc| HandleExercise { return_loc }) + .boxed() + } +} + +proptest! { + #[test] + fn handle_exercise(e in HandleExercise::strat()) { + e.test() + } +} diff --git a/crates/wiggle/tests/handles.witx b/crates/wiggle/tests/handles.witx new file mode 100644 index 0000000000..69c1b0546e --- /dev/null +++ b/crates/wiggle/tests/handles.witx @@ -0,0 +1,12 @@ +(use "errno.witx") + +(typename $fd (handle)) + +(module $handle_examples + (@interface func (export "fd_create") + (result $error $errno) + (result $fd $fd)) + (@interface func (export "fd_consume") + (param $fd $fd) + (result $error $errno)) +) diff --git a/crates/wiggle/tests/ints.rs b/crates/wiggle/tests/ints.rs new file mode 100644 index 0000000000..b4adeb2969 --- /dev/null +++ b/crates/wiggle/tests/ints.rs @@ -0,0 +1,79 @@ +use proptest::prelude::*; +use std::convert::TryFrom; +use wiggle_runtime::{GuestError, GuestMemory}; +use wiggle_test::{impl_errno, HostMemory, MemArea, WasiCtx}; + +wiggle::from_witx!({ + witx: ["tests/ints.witx"], + ctx: WasiCtx, +}); + +impl_errno!(types::Errno); + +impl<'a> ints::Ints for WasiCtx<'a> { + fn cookie_cutter(&self, init_cookie: types::Cookie) -> Result { + let res = if init_cookie == types::Cookie::START { + types::Bool::True + } else { + types::Bool::False + }; + Ok(res) + } +} + +fn cookie_strat() -> impl Strategy { + (0..std::u64::MAX) + .prop_map(|x| types::Cookie::try_from(x).expect("within range of cookie")) + .boxed() +} + +#[derive(Debug)] +struct CookieCutterExercise { + cookie: types::Cookie, + return_ptr_loc: MemArea, +} + +impl CookieCutterExercise { + pub fn strat() -> BoxedStrategy { + (cookie_strat(), HostMemory::mem_area_strat(4)) + .prop_map(|(cookie, return_ptr_loc)| Self { + cookie, + return_ptr_loc, + }) + .boxed() + } + + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + let res = ints::cookie_cutter( + &ctx, + &host_memory, + self.cookie.into(), + self.return_ptr_loc.ptr as i32, + ); + assert_eq!(res, types::Errno::Ok.into(), "cookie cutter errno"); + + let is_cookie_start = host_memory + .ptr::(self.return_ptr_loc.ptr) + .read() + .expect("deref to Bool value"); + + assert_eq!( + if is_cookie_start == types::Bool::True { + true + } else { + false + }, + self.cookie == types::Cookie::START, + "returned Bool should test if input was Cookie::START", + ); + } +} +proptest! { + #[test] + fn cookie_cutter(e in CookieCutterExercise::strat()) { + e.test() + } +} diff --git a/crates/wiggle/tests/ints.witx b/crates/wiggle/tests/ints.witx new file mode 100644 index 0000000000..09dc62f5ec --- /dev/null +++ b/crates/wiggle/tests/ints.witx @@ -0,0 +1,18 @@ +(use "errno.witx") + +(typename $cookie + (int u64 + (const $start 0))) + +(typename $bool + (enum u8 + $false + $true)) + +(module $ints + (@interface func (export "cookie_cutter") + (param $init_cookie $cookie) + (result $error $errno) + (result $is_start $bool) + ) +) diff --git a/crates/wiggle/tests/pointers.rs b/crates/wiggle/tests/pointers.rs new file mode 100644 index 0000000000..188fa9822c --- /dev/null +++ b/crates/wiggle/tests/pointers.rs @@ -0,0 +1,191 @@ +use proptest::prelude::*; +use wiggle_runtime::{GuestError, GuestMemory, GuestPtr}; +use wiggle_test::{impl_errno, HostMemory, MemArea, WasiCtx}; + +wiggle::from_witx!({ + witx: ["tests/pointers.witx"], + ctx: WasiCtx, +}); + +impl_errno!(types::Errno); + +impl<'a> pointers::Pointers for WasiCtx<'a> { + fn pointers_and_enums<'b>( + &self, + input1: types::Excuse, + input2_ptr: GuestPtr<'b, types::Excuse>, + input3_ptr: GuestPtr<'b, types::Excuse>, + input4_ptr_ptr: GuestPtr<'b, GuestPtr<'b, types::Excuse>>, + ) -> Result<(), types::Errno> { + println!("BAZ input1 {:?}", input1); + let input2: types::Excuse = input2_ptr.read().map_err(|e| { + eprintln!("input2_ptr error: {}", e); + types::Errno::InvalidArg + })?; + println!("input2 {:?}", input2); + + // Read enum value from immutable ptr: + let input3 = input3_ptr.read().map_err(|e| { + eprintln!("input3_ptr error: {}", e); + types::Errno::InvalidArg + })?; + println!("input3 {:?}", input3); + + // Write enum to mutable ptr: + input2_ptr.write(input3).map_err(|e| { + eprintln!("input2_ptr error: {}", e); + types::Errno::InvalidArg + })?; + println!("wrote to input2_ref {:?}", input3); + + // Read ptr value from mutable ptr: + let input4_ptr: GuestPtr = input4_ptr_ptr.read().map_err(|e| { + eprintln!("input4_ptr_ptr error: {}", e); + types::Errno::InvalidArg + })?; + + // Read enum value from that ptr: + let input4: types::Excuse = input4_ptr.read().map_err(|e| { + eprintln!("input4_ptr error: {}", e); + types::Errno::InvalidArg + })?; + println!("input4 {:?}", input4); + + // Write ptr value to mutable ptr: + input4_ptr_ptr.write(input2_ptr).map_err(|e| { + eprintln!("input4_ptr_ptr error: {}", e); + types::Errno::InvalidArg + })?; + + Ok(()) + } +} + +fn excuse_strat() -> impl Strategy { + prop_oneof![ + Just(types::Excuse::DogAte), + Just(types::Excuse::Traffic), + Just(types::Excuse::Sleeping), + ] + .boxed() +} + +#[derive(Debug)] +struct PointersAndEnumsExercise { + pub input1: types::Excuse, + pub input2: types::Excuse, + pub input2_loc: MemArea, + pub input3: types::Excuse, + pub input3_loc: MemArea, + pub input4: types::Excuse, + pub input4_loc: MemArea, + pub input4_ptr_loc: MemArea, +} + +impl PointersAndEnumsExercise { + pub fn strat() -> BoxedStrategy { + ( + excuse_strat(), + excuse_strat(), + HostMemory::mem_area_strat(4), + excuse_strat(), + HostMemory::mem_area_strat(4), + excuse_strat(), + HostMemory::mem_area_strat(4), + HostMemory::mem_area_strat(4), + ) + .prop_map( + |( + input1, + input2, + input2_loc, + input3, + input3_loc, + input4, + input4_loc, + input4_ptr_loc, + )| PointersAndEnumsExercise { + input1, + input2, + input2_loc, + input3, + input3_loc, + input4, + input4_loc, + input4_ptr_loc, + }, + ) + .prop_filter("non-overlapping pointers", |e| { + MemArea::non_overlapping_set(&[ + e.input2_loc, + e.input3_loc, + e.input4_loc, + e.input4_ptr_loc, + ]) + }) + .boxed() + } + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + host_memory + .ptr(self.input2_loc.ptr) + .write(self.input2) + .expect("input2 ref_mut"); + + host_memory + .ptr(self.input3_loc.ptr) + .write(self.input3) + .expect("input3 ref_mut"); + + host_memory + .ptr(self.input4_loc.ptr) + .write(self.input4) + .expect("input4 ref_mut"); + + host_memory + .ptr(self.input4_ptr_loc.ptr) + .write(self.input4_loc.ptr) + .expect("input4 ptr ref_mut"); + + let e = pointers::pointers_and_enums( + &ctx, + &host_memory, + self.input1.into(), + self.input2_loc.ptr as i32, + self.input3_loc.ptr as i32, + self.input4_ptr_loc.ptr as i32, + ); + assert_eq!(e, types::Errno::Ok.into(), "errno"); + + // Implementation of pointers_and_enums writes input3 to the input2_loc: + let written_to_input2_loc: i32 = host_memory + .ptr(self.input2_loc.ptr) + .read() + .expect("input2 ref"); + + assert_eq!( + written_to_input2_loc, + self.input3.into(), + "pointers_and_enums written to input2" + ); + + // Implementation of pointers_and_enums writes input2_loc to input4_ptr_loc: + let written_to_input4_ptr: u32 = host_memory + .ptr(self.input4_ptr_loc.ptr) + .read() + .expect("input4_ptr_loc ref"); + + assert_eq!( + written_to_input4_ptr, self.input2_loc.ptr, + "pointers_and_enums written to input4_ptr" + ); + } +} +proptest! { + #[test] + fn pointers_and_enums(e in PointersAndEnumsExercise::strat()) { + e.test(); + } +} diff --git a/crates/wiggle/tests/pointers.witx b/crates/wiggle/tests/pointers.witx new file mode 100644 index 0000000000..9a73a37520 --- /dev/null +++ b/crates/wiggle/tests/pointers.witx @@ -0,0 +1,11 @@ +(use "errno.witx") +(use "excuse.witx") + +(module $pointers + (@interface func (export "pointers_and_enums") + (param $an_excuse $excuse) + (param $an_excuse_by_reference (@witx pointer $excuse)) + (param $a_lamer_excuse (@witx const_pointer $excuse)) + (param $two_layers_of_excuses (@witx pointer (@witx const_pointer $excuse))) + (result $error $errno)) +) diff --git a/crates/wiggle/tests/strings.rs b/crates/wiggle/tests/strings.rs new file mode 100644 index 0000000000..7fca007b4d --- /dev/null +++ b/crates/wiggle/tests/strings.rs @@ -0,0 +1,223 @@ +use proptest::prelude::*; +use wiggle_runtime::{GuestBorrows, GuestError, GuestMemory, GuestPtr}; +use wiggle_test::{impl_errno, HostMemory, MemArea, MemAreas, WasiCtx}; + +wiggle::from_witx!({ + witx: ["tests/strings.witx"], + ctx: WasiCtx, +}); + +impl_errno!(types::Errno); + +impl<'a> strings::Strings for WasiCtx<'a> { + fn hello_string(&self, a_string: &GuestPtr) -> Result { + let mut bc = GuestBorrows::new(); + let s = a_string.as_raw(&mut bc).expect("should be valid string"); + unsafe { + println!("a_string='{}'", &*s); + Ok((*s).len() as u32) + } + } + + fn multi_string( + &self, + a: &GuestPtr, + b: &GuestPtr, + c: &GuestPtr, + ) -> Result { + let mut bc = GuestBorrows::new(); + let sa = a.as_raw(&mut bc).expect("A should be valid string"); + let sb = b.as_raw(&mut bc).expect("B should be valid string"); + let sc = c.as_raw(&mut bc).expect("C should be valid string"); + unsafe { + let total_len = (&*sa).len() + (&*sb).len() + (&*sc).len(); + println!( + "len={}, a='{}', b='{}', c='{}'", + total_len, &*sa, &*sb, &*sc + ); + Ok(total_len as u32) + } + } +} + +fn test_string_strategy() -> impl Strategy { + "\\p{Greek}{1,256}" +} + +#[derive(Debug)] +struct HelloStringExercise { + test_word: String, + string_ptr_loc: MemArea, + return_ptr_loc: MemArea, +} + +impl HelloStringExercise { + pub fn strat() -> BoxedStrategy { + (test_string_strategy(),) + .prop_flat_map(|(test_word,)| { + ( + Just(test_word.clone()), + HostMemory::mem_area_strat(test_word.len() as u32), + HostMemory::mem_area_strat(4), + ) + }) + .prop_map(|(test_word, string_ptr_loc, return_ptr_loc)| Self { + test_word, + string_ptr_loc, + return_ptr_loc, + }) + .prop_filter("non-overlapping pointers", |e| { + MemArea::non_overlapping_set(&[e.string_ptr_loc, e.return_ptr_loc]) + }) + .boxed() + } + + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + // Populate string in guest's memory + let ptr = host_memory.ptr::((self.string_ptr_loc.ptr, self.test_word.len() as u32)); + for (slot, byte) in ptr.as_bytes().iter().zip(self.test_word.bytes()) { + slot.expect("should be valid pointer") + .write(byte) + .expect("failed to write"); + } + + let res = strings::hello_string( + &ctx, + &host_memory, + self.string_ptr_loc.ptr as i32, + self.test_word.len() as i32, + self.return_ptr_loc.ptr as i32, + ); + assert_eq!(res, types::Errno::Ok.into(), "hello string errno"); + + let given = host_memory + .ptr::(self.return_ptr_loc.ptr) + .read() + .expect("deref ptr to return value"); + assert_eq!(self.test_word.len() as u32, given); + } +} +proptest! { + #[test] + fn hello_string(e in HelloStringExercise::strat()) { + e.test() + } +} + +#[derive(Debug)] +struct MultiStringExercise { + a: String, + b: String, + c: String, + sa_ptr_loc: MemArea, + sb_ptr_loc: MemArea, + sc_ptr_loc: MemArea, + return_ptr_loc: MemArea, +} + +impl MultiStringExercise { + pub fn strat() -> BoxedStrategy { + ( + test_string_strategy(), + test_string_strategy(), + test_string_strategy(), + HostMemory::mem_area_strat(4), + ) + .prop_flat_map(|(a, b, c, return_ptr_loc)| { + ( + Just(a.clone()), + Just(b.clone()), + Just(c.clone()), + HostMemory::byte_slice_strat(a.len() as u32, &MemAreas::from([return_ptr_loc])), + Just(return_ptr_loc), + ) + }) + .prop_flat_map(|(a, b, c, sa_ptr_loc, return_ptr_loc)| { + ( + Just(a.clone()), + Just(b.clone()), + Just(c.clone()), + Just(sa_ptr_loc), + HostMemory::byte_slice_strat( + b.len() as u32, + &MemAreas::from([sa_ptr_loc, return_ptr_loc]), + ), + Just(return_ptr_loc), + ) + }) + .prop_flat_map(|(a, b, c, sa_ptr_loc, sb_ptr_loc, return_ptr_loc)| { + ( + Just(a.clone()), + Just(b.clone()), + Just(c.clone()), + Just(sa_ptr_loc), + Just(sb_ptr_loc), + HostMemory::byte_slice_strat( + c.len() as u32, + &MemAreas::from([sa_ptr_loc, sb_ptr_loc, return_ptr_loc]), + ), + Just(return_ptr_loc), + ) + }) + .prop_map( + |(a, b, c, sa_ptr_loc, sb_ptr_loc, sc_ptr_loc, return_ptr_loc)| { + MultiStringExercise { + a, + b, + c, + sa_ptr_loc, + sb_ptr_loc, + sc_ptr_loc, + return_ptr_loc, + } + }, + ) + .boxed() + } + + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + let write_string = |val: &str, loc: MemArea| { + let ptr = host_memory.ptr::((loc.ptr, val.len() as u32)); + for (slot, byte) in ptr.as_bytes().iter().zip(val.bytes()) { + slot.expect("should be valid pointer") + .write(byte) + .expect("failed to write"); + } + }; + + write_string(&self.a, self.sa_ptr_loc); + write_string(&self.b, self.sb_ptr_loc); + write_string(&self.c, self.sc_ptr_loc); + + let res = strings::multi_string( + &ctx, + &host_memory, + self.sa_ptr_loc.ptr as i32, + self.a.len() as i32, + self.sb_ptr_loc.ptr as i32, + self.b.len() as i32, + self.sc_ptr_loc.ptr as i32, + self.c.len() as i32, + self.return_ptr_loc.ptr as i32, + ); + assert_eq!(res, types::Errno::Ok.into(), "multi string errno"); + + let given = host_memory + .ptr::(self.return_ptr_loc.ptr) + .read() + .expect("deref ptr to return value"); + assert_eq!((self.a.len() + self.b.len() + self.c.len()) as u32, given); + } +} +proptest! { + #[test] + fn multi_string(e in MultiStringExercise::strat()) { + e.test() + } +} diff --git a/crates/wiggle/tests/strings.witx b/crates/wiggle/tests/strings.witx new file mode 100644 index 0000000000..b3531e87bb --- /dev/null +++ b/crates/wiggle/tests/strings.witx @@ -0,0 +1,16 @@ +(use "errno.witx") +(module $strings + (@interface func (export "hello_string") + (param $a_string string) + (result $error $errno) + (result $total_bytes u32) + ) + + (@interface func (export "multi_string") + (param $a string) + (param $b string) + (param $c string) + (result $error $errno) + (result $total_bytes u32) + ) +) diff --git a/crates/wiggle/tests/structs.rs b/crates/wiggle/tests/structs.rs new file mode 100644 index 0000000000..0b00057d9a --- /dev/null +++ b/crates/wiggle/tests/structs.rs @@ -0,0 +1,422 @@ +use proptest::prelude::*; +use wiggle_runtime::{GuestError, GuestMemory, GuestPtr}; +use wiggle_test::{impl_errno, HostMemory, MemArea, WasiCtx}; + +wiggle::from_witx!({ + witx: ["tests/structs.witx"], + ctx: WasiCtx, +}); + +impl_errno!(types::Errno); + +impl<'a> structs::Structs for WasiCtx<'a> { + fn sum_of_pair(&self, an_pair: &types::PairInts) -> Result { + Ok(an_pair.first as i64 + an_pair.second as i64) + } + + fn sum_of_pair_of_ptrs(&self, an_pair: &types::PairIntPtrs) -> Result { + let first = an_pair + .first + .read() + .expect("dereferencing GuestPtr should succeed"); + let second = an_pair + .second + .read() + .expect("dereferncing GuestPtr should succeed"); + Ok(first as i64 + second as i64) + } + + fn sum_of_int_and_ptr(&self, an_pair: &types::PairIntAndPtr) -> Result { + let first = an_pair + .first + .read() + .expect("dereferencing GuestPtr should succeed"); + let second = an_pair.second as i64; + Ok(first as i64 + second) + } + + fn return_pair_ints(&self) -> Result { + Ok(types::PairInts { + first: 10, + second: 20, + }) + } + + fn return_pair_of_ptrs<'b>( + &self, + first: GuestPtr<'b, i32>, + second: GuestPtr<'b, i32>, + ) -> Result, types::Errno> { + Ok(types::PairIntPtrs { first, second }) + } +} + +#[derive(Debug)] +struct SumOfPairExercise { + pub input: types::PairInts, + pub input_loc: MemArea, + pub return_loc: MemArea, +} + +impl SumOfPairExercise { + pub fn strat() -> BoxedStrategy { + ( + prop::num::i32::ANY, + prop::num::i32::ANY, + HostMemory::mem_area_strat(8), + HostMemory::mem_area_strat(8), + ) + .prop_map(|(first, second, input_loc, return_loc)| SumOfPairExercise { + input: types::PairInts { first, second }, + input_loc, + return_loc, + }) + .prop_filter("non-overlapping pointers", |e| { + MemArea::non_overlapping_set(&[e.input_loc, e.return_loc]) + }) + .boxed() + } + + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + host_memory + .ptr(self.input_loc.ptr) + .write(self.input.first) + .expect("input ref_mut"); + host_memory + .ptr(self.input_loc.ptr + 4) + .write(self.input.second) + .expect("input ref_mut"); + let sum_err = structs::sum_of_pair( + &ctx, + &host_memory, + self.input_loc.ptr as i32, + self.return_loc.ptr as i32, + ); + + assert_eq!(sum_err, types::Errno::Ok.into(), "sum errno"); + + let return_val: i64 = host_memory + .ptr(self.return_loc.ptr) + .read() + .expect("return ref"); + + assert_eq!( + return_val, + self.input.first as i64 + self.input.second as i64, + "sum return value" + ); + } +} + +proptest! { + #[test] + fn sum_of_pair(e in SumOfPairExercise::strat()) { + e.test(); + } +} + +#[derive(Debug)] +struct SumPairPtrsExercise { + input_first: i32, + input_second: i32, + input_first_loc: MemArea, + input_second_loc: MemArea, + input_struct_loc: MemArea, + return_loc: MemArea, +} + +impl SumPairPtrsExercise { + pub fn strat() -> BoxedStrategy { + ( + prop::num::i32::ANY, + prop::num::i32::ANY, + HostMemory::mem_area_strat(4), + HostMemory::mem_area_strat(4), + HostMemory::mem_area_strat(8), + HostMemory::mem_area_strat(8), + ) + .prop_map( + |( + input_first, + input_second, + input_first_loc, + input_second_loc, + input_struct_loc, + return_loc, + )| SumPairPtrsExercise { + input_first, + input_second, + input_first_loc, + input_second_loc, + input_struct_loc, + return_loc, + }, + ) + .prop_filter("non-overlapping pointers", |e| { + MemArea::non_overlapping_set(&[ + e.input_first_loc, + e.input_second_loc, + e.input_struct_loc, + e.return_loc, + ]) + }) + .boxed() + } + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + host_memory + .ptr(self.input_first_loc.ptr) + .write(self.input_first) + .expect("input_first ref"); + host_memory + .ptr(self.input_second_loc.ptr) + .write(self.input_second) + .expect("input_second ref"); + + host_memory + .ptr(self.input_struct_loc.ptr) + .write(self.input_first_loc.ptr) + .expect("input_struct ref"); + host_memory + .ptr(self.input_struct_loc.ptr + 4) + .write(self.input_second_loc.ptr) + .expect("input_struct ref"); + + let res = structs::sum_of_pair_of_ptrs( + &ctx, + &host_memory, + self.input_struct_loc.ptr as i32, + self.return_loc.ptr as i32, + ); + + assert_eq!(res, types::Errno::Ok.into(), "sum of pair of ptrs errno"); + + let doubled: i64 = host_memory + .ptr(self.return_loc.ptr) + .read() + .expect("return ref"); + + assert_eq!( + doubled, + (self.input_first as i64) + (self.input_second as i64), + "sum of pair of ptrs return val" + ); + } +} +proptest! { + #[test] + fn sum_of_pair_of_ptrs(e in SumPairPtrsExercise::strat()) { + e.test() + } +} + +#[derive(Debug)] +struct SumIntAndPtrExercise { + input_first: i32, + input_second: i32, + input_first_loc: MemArea, + input_struct_loc: MemArea, + return_loc: MemArea, +} + +impl SumIntAndPtrExercise { + pub fn strat() -> BoxedStrategy { + ( + prop::num::i32::ANY, + prop::num::i32::ANY, + HostMemory::mem_area_strat(4), + HostMemory::mem_area_strat(8), + HostMemory::mem_area_strat(8), + ) + .prop_map( + |(input_first, input_second, input_first_loc, input_struct_loc, return_loc)| { + SumIntAndPtrExercise { + input_first, + input_second, + input_first_loc, + input_struct_loc, + return_loc, + } + }, + ) + .prop_filter("non-overlapping pointers", |e| { + MemArea::non_overlapping_set(&[e.input_first_loc, e.input_struct_loc, e.return_loc]) + }) + .boxed() + } + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + host_memory + .ptr(self.input_first_loc.ptr) + .write(self.input_first) + .expect("input_first ref"); + host_memory + .ptr(self.input_struct_loc.ptr) + .write(self.input_first_loc.ptr) + .expect("input_struct ref"); + host_memory + .ptr(self.input_struct_loc.ptr + 4) + .write(self.input_second) + .expect("input_struct ref"); + + let res = structs::sum_of_int_and_ptr( + &ctx, + &host_memory, + self.input_struct_loc.ptr as i32, + self.return_loc.ptr as i32, + ); + + assert_eq!(res, types::Errno::Ok.into(), "sum of int and ptr errno"); + + let doubled: i64 = host_memory + .ptr(self.return_loc.ptr) + .read() + .expect("return ref"); + + assert_eq!( + doubled, + (self.input_first as i64) + (self.input_second as i64), + "sum of pair of ptrs return val" + ); + } +} +proptest! { + #[test] + fn sum_of_int_and_ptr(e in SumIntAndPtrExercise::strat()) { + e.test() + } +} + +#[derive(Debug)] +struct ReturnPairInts { + pub return_loc: MemArea, +} + +impl ReturnPairInts { + pub fn strat() -> BoxedStrategy { + HostMemory::mem_area_strat(8) + .prop_map(|return_loc| ReturnPairInts { return_loc }) + .boxed() + } + + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + let err = structs::return_pair_ints(&ctx, &host_memory, self.return_loc.ptr as i32); + + assert_eq!(err, types::Errno::Ok.into(), "return struct errno"); + + let return_struct: types::PairInts = host_memory + .ptr(self.return_loc.ptr) + .read() + .expect("return ref"); + + assert_eq!( + return_struct, + types::PairInts { + first: 10, + second: 20 + }, + "return_pair_ints return value" + ); + } +} + +proptest! { + #[test] + fn return_pair_ints(e in ReturnPairInts::strat()) { + e.test(); + } +} + +#[derive(Debug)] +struct ReturnPairPtrsExercise { + input_first: i32, + input_second: i32, + input_first_loc: MemArea, + input_second_loc: MemArea, + return_loc: MemArea, +} + +impl ReturnPairPtrsExercise { + pub fn strat() -> BoxedStrategy { + ( + prop::num::i32::ANY, + prop::num::i32::ANY, + HostMemory::mem_area_strat(4), + HostMemory::mem_area_strat(4), + HostMemory::mem_area_strat(8), + ) + .prop_map( + |(input_first, input_second, input_first_loc, input_second_loc, return_loc)| { + ReturnPairPtrsExercise { + input_first, + input_second, + input_first_loc, + input_second_loc, + return_loc, + } + }, + ) + .prop_filter("non-overlapping pointers", |e| { + MemArea::non_overlapping_set(&[e.input_first_loc, e.input_second_loc, e.return_loc]) + }) + .boxed() + } + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + host_memory + .ptr(self.input_first_loc.ptr) + .write(self.input_first) + .expect("input_first ref"); + host_memory + .ptr(self.input_second_loc.ptr) + .write(self.input_second) + .expect("input_second ref"); + + let res = structs::return_pair_of_ptrs( + &ctx, + &host_memory, + self.input_first_loc.ptr as i32, + self.input_second_loc.ptr as i32, + self.return_loc.ptr as i32, + ); + + assert_eq!(res, types::Errno::Ok.into(), "return pair of ptrs errno"); + + let ptr_pair_int_ptrs: types::PairIntPtrs<'_> = host_memory + .ptr(self.return_loc.ptr) + .read() + .expect("failed to read return location"); + let ret_first_ptr = ptr_pair_int_ptrs.first; + let ret_second_ptr = ptr_pair_int_ptrs.second; + assert_eq!( + self.input_first, + ret_first_ptr + .read() + .expect("deref extracted ptr to first element") + ); + assert_eq!( + self.input_second, + ret_second_ptr + .read() + .expect("deref extracted ptr to second element") + ); + } +} +proptest! { + #[test] + fn return_pair_of_ptrs(e in ReturnPairPtrsExercise::strat()) { + e.test() + } +} diff --git a/crates/wiggle/tests/structs.witx b/crates/wiggle/tests/structs.witx new file mode 100644 index 0000000000..0542bc68fa --- /dev/null +++ b/crates/wiggle/tests/structs.witx @@ -0,0 +1,40 @@ + +(use "errno.witx") + +(typename $pair_ints + (struct + (field $first s32) + (field $second s32))) + +(typename $pair_int_ptrs + (struct + (field $first (@witx const_pointer s32)) + (field $second (@witx const_pointer s32)))) + +(typename $pair_int_and_ptr + (struct + (field $first (@witx const_pointer s32)) + (field $second s32))) + +(module $structs + (@interface func (export "sum_of_pair") + (param $an_pair $pair_ints) + (result $error $errno) + (result $doubled s64)) + (@interface func (export "sum_of_pair_of_ptrs") + (param $an_pair $pair_int_ptrs) + (result $error $errno) + (result $doubled s64)) + (@interface func (export "sum_of_int_and_ptr") + (param $an_pair $pair_int_and_ptr) + (result $error $errno) + (result $double s64)) + (@interface func (export "return_pair_ints") + (result $error $errno) + (result $an_pair $pair_ints)) + (@interface func (export "return_pair_of_ptrs") + (param $first (@witx const_pointer s32)) + (param $second (@witx const_pointer s32)) + (result $error $errno) + (result $an_pair $pair_int_ptrs)) +) diff --git a/crates/wiggle/tests/typenames.witx b/crates/wiggle/tests/typenames.witx new file mode 100644 index 0000000000..1351fc4e13 --- /dev/null +++ b/crates/wiggle/tests/typenames.witx @@ -0,0 +1,746 @@ +;; Type names used by low-level WASI interfaces. +;; +;; Some content here is derived from [CloudABI](https://github.com/NuxiNL/cloudabi). +;; +;; This is a `witx` file. See [here](https://github.com/WebAssembly/WASI/tree/master/docs/witx.md) +;; for an explanation of what that means. + +(typename $size u32) + +;;; Non-negative file size or length of a region within a file. +(typename $filesize u64) + +;;; Timestamp in nanoseconds. +(typename $timestamp u64) + +;;; Identifiers for clocks. +(typename $clockid + (enum u32 + ;;; The clock measuring real time. Time value zero corresponds with + ;;; 1970-01-01T00:00:00Z. + $realtime + ;;; The store-wide monotonic clock, which is defined as a clock measuring + ;;; real time, whose value cannot be adjusted and which cannot have negative + ;;; clock jumps. The epoch of this clock is undefined. The absolute time + ;;; value of this clock therefore has no meaning. + $monotonic + ;;; The CPU-time clock associated with the current process. + $process_cputime_id + ;;; The CPU-time clock associated with the current thread. + $thread_cputime_id + ) +) + +;;; Error codes returned by functions. +;;; Not all of these error codes are returned by the functions provided by this +;;; API; some are used in higher-level library layers, and others are provided +;;; merely for alignment with POSIX. +(typename $errno + (enum u16 + ;;; No error occurred. System call completed successfully. + $success + ;;; Argument list too long. + $2big + ;;; Permission denied. + $acces + ;;; Address in use. + $addrinuse + ;;; Address not available. + $addrnotavail + ;;; Address family not supported. + $afnosupport + ;;; Resource unavailable, or operation would block. + $again + ;;; Connection already in progress. + $already + ;;; Bad file descriptor. + $badf + ;;; Bad message. + $badmsg + ;;; Device or resource busy. + $busy + ;;; Operation canceled. + $canceled + ;;; No child processes. + $child + ;;; Connection aborted. + $connaborted + ;;; Connection refused. + $connrefused + ;;; Connection reset. + $connreset + ;;; Resource deadlock would occur. + $deadlk + ;;; Destination address required. + $destaddrreq + ;;; Mathematics argument out of domain of function. + $dom + ;;; Reserved. + $dquot + ;;; File exists. + $exist + ;;; Bad address. + $fault + ;;; File too large. + $fbig + ;;; Host is unreachable. + $hostunreach + ;;; Identifier removed. + $idrm + ;;; Illegal byte sequence. + $ilseq + ;;; Operation in progress. + $inprogress + ;;; Interrupted function. + $intr + ;;; Invalid argument. + $inval + ;;; I/O error. + $io + ;;; Socket is connected. + $isconn + ;;; Is a directory. + $isdir + ;;; Too many levels of symbolic links. + $loop + ;;; File descriptor value too large. + $mfile + ;;; Too many links. + $mlink + ;;; Message too large. + $msgsize + ;;; Reserved. + $multihop + ;;; Filename too long. + $nametoolong + ;;; Network is down. + $netdown + ;;; Connection aborted by network. + $netreset + ;;; Network unreachable. + $netunreach + ;;; Too many files open in system. + $nfile + ;;; No buffer space available. + $nobufs + ;;; No such device. + $nodev + ;;; No such file or directory. + $noent + ;;; Executable file format error. + $noexec + ;;; No locks available. + $nolck + ;;; Reserved. + $nolink + ;;; Not enough space. + $nomem + ;;; No message of the desired type. + $nomsg + ;;; Protocol not available. + $noprotoopt + ;;; No space left on device. + $nospc + ;;; Function not supported. + $nosys + ;;; The socket is not connected. + $notconn + ;;; Not a directory or a symbolic link to a directory. + $notdir + ;;; Directory not empty. + $notempty + ;;; State not recoverable. + $notrecoverable + ;;; Not a socket. + $notsock + ;;; Not supported, or operation not supported on socket. + $notsup + ;;; Inappropriate I/O control operation. + $notty + ;;; No such device or address. + $nxio + ;;; Value too large to be stored in data type. + $overflow + ;;; Previous owner died. + $ownerdead + ;;; Operation not permitted. + $perm + ;;; Broken pipe. + $pipe + ;;; Protocol error. + $proto + ;;; Protocol not supported. + $protonosupport + ;;; Protocol wrong type for socket. + $prototype + ;;; Result too large. + $range + ;;; Read-only file system. + $rofs + ;;; Invalid seek. + $spipe + ;;; No such process. + $srch + ;;; Reserved. + $stale + ;;; Connection timed out. + $timedout + ;;; Text file busy. + $txtbsy + ;;; Cross-device link. + $xdev + ;;; Extension: Capabilities insufficient. + $notcapable + ) +) + +;;; File descriptor rights, determining which actions may be performed. +(typename $rights + (flags u64 + ;;; The right to invoke `fd_datasync`. + ;; + ;;; If `path_open` is set, includes the right to invoke + ;;; `path_open` with `fdflags::dsync`. + $fd_datasync + ;;; The right to invoke `fd_read` and `sock_recv`. + ;; + ;;; If `rights::fd_seek` is set, includes the right to invoke `fd_pread`. + $fd_read + ;;; The right to invoke `fd_seek`. This flag implies `rights::fd_tell`. + $fd_seek + ;;; The right to invoke `fd_fdstat_set_flags`. + $fd_fdstat_set_flags + ;;; The right to invoke `fd_sync`. + ;; + ;;; If `path_open` is set, includes the right to invoke + ;;; `path_open` with `fdflags::rsync` and `fdflags::dsync`. + $fd_sync + ;;; The right to invoke `fd_seek` in such a way that the file offset + ;;; remains unaltered (i.e., `whence::cur` with offset zero), or to + ;;; invoke `fd_tell`. + $fd_tell + ;;; The right to invoke `fd_write` and `sock_send`. + ;;; If `rights::fd_seek` is set, includes the right to invoke `fd_pwrite`. + $fd_write + ;;; The right to invoke `fd_advise`. + $fd_advise + ;;; The right to invoke `fd_allocate`. + $fd_allocate + ;;; The right to invoke `path_create_directory`. + $path_create_directory + ;;; If `path_open` is set, the right to invoke `path_open` with `oflags::creat`. + $path_create_file + ;;; The right to invoke `path_link` with the file descriptor as the + ;;; source directory. + $path_link_source + ;;; The right to invoke `path_link` with the file descriptor as the + ;;; target directory. + $path_link_target + ;;; The right to invoke `path_open`. + $path_open + ;;; The right to invoke `fd_readdir`. + $fd_readdir + ;;; The right to invoke `path_readlink`. + $path_readlink + ;;; The right to invoke `path_rename` with the file descriptor as the source directory. + $path_rename_source + ;;; The right to invoke `path_rename` with the file descriptor as the target directory. + $path_rename_target + ;;; The right to invoke `path_filestat_get`. + $path_filestat_get + ;;; The right to change a file's size (there is no `path_filestat_set_size`). + ;;; If `path_open` is set, includes the right to invoke `path_open` with `oflags::trunc`. + $path_filestat_set_size + ;;; The right to invoke `path_filestat_set_times`. + $path_filestat_set_times + ;;; The right to invoke `fd_filestat_get`. + $fd_filestat_get + ;;; The right to invoke `fd_filestat_set_size`. + $fd_filestat_set_size + ;;; The right to invoke `fd_filestat_set_times`. + $fd_filestat_set_times + ;;; The right to invoke `path_symlink`. + $path_symlink + ;;; The right to invoke `path_remove_directory`. + $path_remove_directory + ;;; The right to invoke `path_unlink_file`. + $path_unlink_file + ;;; If `rights::fd_read` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_read`. + ;;; If `rights::fd_write` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_write`. + $poll_fd_readwrite + ;;; The right to invoke `sock_shutdown`. + $sock_shutdown + ) +) + +;;; A file descriptor handle. +(typename $fd (handle)) + +;;; A region of memory for scatter/gather reads. +(typename $iovec + (struct + ;;; The address of the buffer to be filled. + (field $buf (@witx pointer u8)) + ;;; The length of the buffer to be filled. + (field $buf_len $size) + ) +) + +;;; A region of memory for scatter/gather writes. +(typename $ciovec + (struct + ;;; The address of the buffer to be written. + (field $buf (@witx const_pointer u8)) + ;;; The length of the buffer to be written. + (field $buf_len $size) + ) +) + +(typename $iovec_array (array $iovec)) +(typename $ciovec_array (array $ciovec)) + +;;; Relative offset within a file. +(typename $filedelta s64) + +;;; The position relative to which to set the offset of the file descriptor. +(typename $whence + (enum u8 + ;;; Seek relative to start-of-file. + $set + ;;; Seek relative to current position. + $cur + ;;; Seek relative to end-of-file. + $end + ) +) + +;;; A reference to the offset of a directory entry. +;;; +;;; The value 0 signifies the start of the directory. +(typename $dircookie u64) + +;;; The type for the $d_namlen field of $dirent. +(typename $dirnamlen u32) + +;;; File serial number that is unique within its file system. +(typename $inode u64) + +;;; The type of a file descriptor or file. +(typename $filetype + (enum u8 + ;;; The type of the file descriptor or file is unknown or is different from any of the other types specified. + $unknown + ;;; The file descriptor or file refers to a block device inode. + $block_device + ;;; The file descriptor or file refers to a character device inode. + $character_device + ;;; The file descriptor or file refers to a directory inode. + $directory + ;;; The file descriptor or file refers to a regular file inode. + $regular_file + ;;; The file descriptor or file refers to a datagram socket. + $socket_dgram + ;;; The file descriptor or file refers to a byte-stream socket. + $socket_stream + ;;; The file refers to a symbolic link inode. + $symbolic_link + ) +) + +;;; A directory entry. +(typename $dirent + (struct + ;;; The offset of the next directory entry stored in this directory. + (field $d_next $dircookie) + ;;; The serial number of the file referred to by this directory entry. + (field $d_ino $inode) + ;;; The length of the name of the directory entry. + (field $d_namlen $dirnamlen) + ;;; The type of the file referred to by this directory entry. + (field $d_type $filetype) + ) +) + +;;; File or memory access pattern advisory information. +(typename $advice + (enum u8 + ;;; The application has no advice to give on its behavior with respect to the specified data. + $normal + ;;; The application expects to access the specified data sequentially from lower offsets to higher offsets. + $sequential + ;;; The application expects to access the specified data in a random order. + $random + ;;; The application expects to access the specified data in the near future. + $willneed + ;;; The application expects that it will not access the specified data in the near future. + $dontneed + ;;; The application expects to access the specified data once and then not reuse it thereafter. + $noreuse + ) +) + +;;; File descriptor flags. +(typename $fdflags + (flags u16 + ;;; Append mode: Data written to the file is always appended to the file's end. + $append + ;;; Write according to synchronized I/O data integrity completion. Only the data stored in the file is synchronized. + $dsync + ;;; Non-blocking mode. + $nonblock + ;;; Synchronized read I/O operations. + $rsync + ;;; Write according to synchronized I/O file integrity completion. In + ;;; addition to synchronizing the data stored in the file, the implementation + ;;; may also synchronously update the file's metadata. + $sync + ) +) + +;;; File descriptor attributes. +(typename $fdstat + (struct + ;;; File type. + (field $fs_filetype $filetype) + ;;; File descriptor flags. + (field $fs_flags $fdflags) + ;;; Rights that apply to this file descriptor. + (field $fs_rights_base $rights) + ;;; Maximum set of rights that may be installed on new file descriptors that + ;;; are created through this file descriptor, e.g., through `path_open`. + (field $fs_rights_inheriting $rights) + ) +) + +;;; Identifier for a device containing a file system. Can be used in combination +;;; with `inode` to uniquely identify a file or directory in the filesystem. +(typename $device u64) + +;;; Which file time attributes to adjust. +(typename $fstflags + (flags u16 + ;;; Adjust the last data access timestamp to the value stored in `filestat::atim`. + $atim + ;;; Adjust the last data access timestamp to the time of clock `clockid::realtime`. + $atim_now + ;;; Adjust the last data modification timestamp to the value stored in `filestat::mtim`. + $mtim + ;;; Adjust the last data modification timestamp to the time of clock `clockid::realtime`. + $mtim_now + ) +) + +;;; Flags determining the method of how paths are resolved. +(typename $lookupflags + (flags u32 + ;;; As long as the resolved path corresponds to a symbolic link, it is expanded. + $symlink_follow + ) +) + +;;; Open flags used by `path_open`. +(typename $oflags + (flags u16 + ;;; Create file if it does not exist. + $creat + ;;; Fail if not a directory. + $directory + ;;; Fail if file already exists. + $excl + ;;; Truncate file to size 0. + $trunc + ) +) + +;;; Number of hard links to an inode. +(typename $linkcount u64) + +;;; File attributes. +(typename $filestat + (struct + ;;; Device ID of device containing the file. + (field $dev $device) + ;;; File serial number. + (field $ino $inode) + ;;; File type. + (field $filetype $filetype) + ;;; Number of hard links to the file. + (field $nlink $linkcount) + ;;; For regular files, the file size in bytes. For symbolic links, the length in bytes of the pathname contained in the symbolic link. + (field $size $filesize) + ;;; Last data access timestamp. + (field $atim $timestamp) + ;;; Last data modification timestamp. + (field $mtim $timestamp) + ;;; Last file status change timestamp. + (field $ctim $timestamp) + ) +) + +;;; User-provided value that may be attached to objects that is retained when +;;; extracted from the implementation. +(typename $userdata u64) + +;;; Type of a subscription to an event or its occurrence. +(typename $eventtype + (enum u8 + ;;; The time value of clock `subscription_clock::id` has + ;;; reached timestamp `subscription_clock::timeout`. + $clock + ;;; File descriptor `subscription_fd_readwrite::file_descriptor` has data + ;;; available for reading. This event always triggers for regular files. + $fd_read + ;;; File descriptor `subscription_fd_readwrite::file_descriptor` has capacity + ;;; available for writing. This event always triggers for regular files. + $fd_write + ) +) + +;;; The state of the file descriptor subscribed to with +;;; `eventtype::fd_read` or `eventtype::fd_write`. +(typename $eventrwflags + (flags u16 + ;;; The peer of this socket has closed or disconnected. + $fd_readwrite_hangup + ) +) + +;;; The contents of an $event when type is `eventtype::fd_read` or +;;; `eventtype::fd_write`. +(typename $event_fd_readwrite + (struct + ;;; The number of bytes available for reading or writing. + (field $nbytes $filesize) + ;;; The state of the file descriptor. + (field $flags $eventrwflags) + ) +) + +;;; An event that occurred. +(typename $event + (struct + ;;; User-provided value that got attached to `subscription::userdata`. + (field $userdata $userdata) + ;;; If non-zero, an error that occurred while processing the subscription request. + (field $error $errno) + ;;; The type of event that occured + (field $type $eventtype) + ;;; The contents of the event, if it is an `eventtype::fd_read` or + ;;; `eventtype::fd_write`. `eventtype::clock` events ignore this field. + (field $fd_readwrite $event_fd_readwrite) + ) +) + +;;; Flags determining how to interpret the timestamp provided in +;;; `subscription_clock::timeout`. +(typename $subclockflags + (flags u16 + ;;; If set, treat the timestamp provided in + ;;; `subscription_clock::timeout` as an absolute timestamp of clock + ;;; `subscription_clock::id`. If clear, treat the timestamp + ;;; provided in `subscription_clock::timeout` relative to the + ;;; current time value of clock `subscription_clock::id`. + $subscription_clock_abstime + ) +) + +;;; The contents of a `subscription` when type is `eventtype::clock`. +(typename $subscription_clock + (struct + ;;; The clock against which to compare the timestamp. + (field $id $clockid) + ;;; The absolute or relative timestamp. + (field $timeout $timestamp) + ;;; The amount of time that the implementation may wait additionally + ;;; to coalesce with other events. + (field $precision $timestamp) + ;;; Flags specifying whether the timeout is absolute or relative + (field $flags $subclockflags) + ) +) + +;;; The contents of a `subscription` when type is type is +;;; `eventtype::fd_read` or `eventtype::fd_write`. +(typename $subscription_fd_readwrite + (struct + ;;; The file descriptor on which to wait for it to become ready for reading or writing. + (field $file_descriptor $fd) + ) +) + +;;; The contents of a `subscription`. +(typename $subscription_u + (union $eventtype + (field $clock $subscription_clock) + (field $fd_read $subscription_fd_readwrite) + (field $fd_write $subscription_fd_readwrite) + ) +) + +;;; Subscription to an event. +(typename $subscription + (struct + ;;; User-provided value that is attached to the subscription in the + ;;; implementation and returned through `event::userdata`. + (field $userdata $userdata) + ;;; The type of the event to which to subscribe, and its contents + (field $u $subscription_u) + ) +) + +;;; Exit code generated by a process when exiting. +(typename $exitcode u32) + +;;; Signal condition. +(typename $signal + (enum u8 + ;;; No signal. Note that POSIX has special semantics for `kill(pid, 0)`, + ;;; so this value is reserved. + $none + ;;; Hangup. + ;;; Action: Terminates the process. + $hup + ;;; Terminate interrupt signal. + ;;; Action: Terminates the process. + $int + ;;; Terminal quit signal. + ;;; Action: Terminates the process. + $quit + ;;; Illegal instruction. + ;;; Action: Terminates the process. + $ill + ;;; Trace/breakpoint trap. + ;;; Action: Terminates the process. + $trap + ;;; Process abort signal. + ;;; Action: Terminates the process. + $abrt + ;;; Access to an undefined portion of a memory object. + ;;; Action: Terminates the process. + $bus + ;;; Erroneous arithmetic operation. + ;;; Action: Terminates the process. + $fpe + ;;; Kill. + ;;; Action: Terminates the process. + $kill + ;;; User-defined signal 1. + ;;; Action: Terminates the process. + $usr1 + ;;; Invalid memory reference. + ;;; Action: Terminates the process. + $segv + ;;; User-defined signal 2. + ;;; Action: Terminates the process. + $usr2 + ;;; Write on a pipe with no one to read it. + ;;; Action: Ignored. + $pipe + ;;; Alarm clock. + ;;; Action: Terminates the process. + $alrm + ;;; Termination signal. + ;;; Action: Terminates the process. + $term + ;;; Child process terminated, stopped, or continued. + ;;; Action: Ignored. + $chld + ;;; Continue executing, if stopped. + ;;; Action: Continues executing, if stopped. + $cont + ;;; Stop executing. + ;;; Action: Stops executing. + $stop + ;;; Terminal stop signal. + ;;; Action: Stops executing. + $tstp + ;;; Background process attempting read. + ;;; Action: Stops executing. + $ttin + ;;; Background process attempting write. + ;;; Action: Stops executing. + $ttou + ;;; High bandwidth data is available at a socket. + ;;; Action: Ignored. + $urg + ;;; CPU time limit exceeded. + ;;; Action: Terminates the process. + $xcpu + ;;; File size limit exceeded. + ;;; Action: Terminates the process. + $xfsz + ;;; Virtual timer expired. + ;;; Action: Terminates the process. + $vtalrm + ;;; Profiling timer expired. + ;;; Action: Terminates the process. + $prof + ;;; Window changed. + ;;; Action: Ignored. + $winch + ;;; I/O possible. + ;;; Action: Terminates the process. + $poll + ;;; Power failure. + ;;; Action: Terminates the process. + $pwr + ;;; Bad system call. + ;;; Action: Terminates the process. + $sys + ) +) + +;;; Flags provided to `sock_recv`. +(typename $riflags + (flags u16 + ;;; Returns the message without removing it from the socket's receive queue. + $recv_peek + ;;; On byte-stream sockets, block until the full amount of data can be returned. + $recv_waitall + ) +) + +;;; Flags returned by `sock_recv`. +(typename $roflags + (flags u16 + ;;; Returned by `sock_recv`: Message data has been truncated. + $recv_data_truncated + ) +) + +;;; Flags provided to `sock_send`. As there are currently no flags +;;; defined, it must be set to zero. +(typename $siflags u16) + +;;; Which channels on a socket to shut down. +(typename $sdflags + (flags u8 + ;;; Disables further receive operations. + $rd + ;;; Disables further send operations. + $wr + ) +) + +;;; Identifiers for preopened capabilities. +(typename $preopentype + (enum u8 + ;;; A pre-opened directory. + $dir + ) +) + +;;; The contents of a $prestat when type is `preopentype::dir`. +(typename $prestat_dir + (struct + ;;; The length of the directory name for use with `fd_prestat_dir_name`. + (field $pr_name_len $size) + ) +) + +;;; Information about a pre-opened capability. +(typename $prestat + (union $preopentype + (field $dir $prestat_dir) + ) +) diff --git a/crates/wiggle/tests/union.rs b/crates/wiggle/tests/union.rs new file mode 100644 index 0000000000..b2510e4ef9 --- /dev/null +++ b/crates/wiggle/tests/union.rs @@ -0,0 +1,255 @@ +use proptest::prelude::*; +use wiggle_runtime::{GuestError, GuestMemory, GuestType}; +use wiggle_test::{impl_errno, HostMemory, MemArea, WasiCtx}; + +wiggle::from_witx!({ + witx: ["tests/union.witx"], + ctx: WasiCtx, +}); + +impl_errno!(types::Errno); + +// Avoid panics on overflow +fn mult_lose_overflow(a: i32, b: u32) -> i32 { + let a_64: i64 = a as i64; + let b_64: i64 = b as i64; + let product = a_64 * b_64; + product as i32 +} + +// Avoid assert_eq(NaN, NaN) failures +fn mult_zero_nan(a: f32, b: u32) -> f32 { + if a.is_nan() { + 0.0 + } else { + let product = a * b as f32; + if product.is_nan() { + 0.0 + } else { + product + } + } +} + +impl<'a> union_example::UnionExample for WasiCtx<'a> { + fn get_tag(&self, u: &types::Reason) -> Result { + println!("GET TAG: {:?}", u); + match u { + types::Reason::DogAte { .. } => Ok(types::Excuse::DogAte), + types::Reason::Traffic { .. } => Ok(types::Excuse::Traffic), + types::Reason::Sleeping { .. } => Ok(types::Excuse::Sleeping), + } + } + fn reason_mult(&self, u: &types::ReasonMut<'_>, multiply_by: u32) -> Result<(), types::Errno> { + match u { + types::ReasonMut::DogAte(fptr) => { + let val = fptr.read().expect("valid pointer"); + println!("REASON MULT DogAte({})", val); + fptr.write(mult_zero_nan(val, multiply_by)) + .expect("valid pointer"); + } + types::ReasonMut::Traffic(iptr) => { + let val = iptr.read().expect("valid pointer"); + println!("REASON MULT Traffic({})", val); + iptr.write(mult_lose_overflow(val, multiply_by)) + .expect("valid pointer"); + } + types::ReasonMut::Sleeping => { + println!("REASON MULT Sleeping"); + } + } + Ok(()) + } +} + +fn reason_strat() -> impl Strategy { + prop_oneof![ + prop::num::f32::ANY.prop_map(|v| types::Reason::DogAte(v)), + prop::num::i32::ANY.prop_map(|v| types::Reason::Traffic(v)), + Just(types::Reason::Sleeping), + ] + .boxed() +} + +fn reason_tag(r: &types::Reason) -> types::Excuse { + match r { + types::Reason::DogAte { .. } => types::Excuse::DogAte, + types::Reason::Traffic { .. } => types::Excuse::Traffic, + types::Reason::Sleeping { .. } => types::Excuse::Sleeping, + } +} + +#[derive(Debug)] +struct GetTagExercise { + pub input: types::Reason, + pub input_loc: MemArea, + pub return_loc: MemArea, +} + +impl GetTagExercise { + pub fn strat() -> BoxedStrategy { + ( + reason_strat(), + HostMemory::mem_area_strat(types::Reason::guest_size()), + HostMemory::mem_area_strat(types::Excuse::guest_size()), + ) + .prop_map(|(input, input_loc, return_loc)| GetTagExercise { + input, + input_loc, + return_loc, + }) + .prop_filter("non-overlapping pointers", |e| { + MemArea::non_overlapping_set(&[e.input_loc, e.return_loc]) + }) + .boxed() + } + + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + let discriminant: u8 = reason_tag(&self.input).into(); + host_memory + .ptr(self.input_loc.ptr) + .write(discriminant) + .expect("input discriminant ptr"); + match self.input { + types::Reason::DogAte(f) => { + host_memory + .ptr(self.input_loc.ptr + 4) + .write(f) + .expect("input contents ref_mut"); + } + types::Reason::Traffic(v) => host_memory + .ptr(self.input_loc.ptr + 4) + .write(v) + .expect("input contents ref_mut"), + types::Reason::Sleeping => {} // Do nothing + } + let e = union_example::get_tag( + &ctx, + &host_memory, + self.input_loc.ptr as i32, + self.return_loc.ptr as i32, + ); + + assert_eq!(e, types::Errno::Ok.into(), "get_tag errno"); + + let return_val: types::Excuse = host_memory + .ptr(self.return_loc.ptr) + .read() + .expect("return ref"); + + assert_eq!(return_val, reason_tag(&self.input), "get_tag return value"); + } +} + +proptest! { + #[test] + fn get_tag(e in GetTagExercise::strat()) { + e.test(); + } +} + +#[derive(Debug)] +struct ReasonMultExercise { + pub input: types::Reason, + pub input_loc: MemArea, + pub input_pointee_loc: MemArea, + pub multiply_by: u32, +} + +impl ReasonMultExercise { + pub fn strat() -> BoxedStrategy { + ( + reason_strat(), + HostMemory::mem_area_strat(types::Reason::guest_size()), + HostMemory::mem_area_strat(4), + prop::num::u32::ANY, + ) + .prop_map( + |(input, input_loc, input_pointee_loc, multiply_by)| ReasonMultExercise { + input, + input_loc, + input_pointee_loc, + multiply_by, + }, + ) + .prop_filter("non-overlapping pointers", |e| { + MemArea::non_overlapping_set(&[e.input_loc, e.input_pointee_loc]) + }) + .boxed() + } + + pub fn test(&self) { + let ctx = WasiCtx::new(); + let host_memory = HostMemory::new(); + + let discriminant: u8 = reason_tag(&self.input).into(); + host_memory + .ptr(self.input_loc.ptr) + .write(discriminant) + .expect("input discriminant ref_mut"); + host_memory + .ptr(self.input_loc.ptr + 4) + .write(self.input_pointee_loc.ptr) + .expect("input pointer ref_mut"); + + match self.input { + types::Reason::DogAte(f) => { + host_memory + .ptr(self.input_pointee_loc.ptr) + .write(f) + .expect("input contents ref_mut"); + } + types::Reason::Traffic(v) => { + host_memory + .ptr(self.input_pointee_loc.ptr) + .write(v) + .expect("input contents ref_mut"); + } + types::Reason::Sleeping => {} // Do nothing + } + let e = union_example::reason_mult( + &ctx, + &host_memory, + self.input_loc.ptr as i32, + self.multiply_by as i32, + ); + + assert_eq!(e, types::Errno::Ok.into(), "reason_mult errno"); + + match self.input { + types::Reason::DogAte(f) => { + let f_result: f32 = host_memory + .ptr(self.input_pointee_loc.ptr) + .read() + .expect("input contents ref_mut"); + assert_eq!( + mult_zero_nan(f, self.multiply_by), + f_result, + "DogAte result" + ) + } + types::Reason::Traffic(v) => { + let v_result: i32 = host_memory + .ptr(self.input_pointee_loc.ptr) + .read() + .expect("input contents ref_mut"); + assert_eq!( + mult_lose_overflow(v, self.multiply_by), + v_result, + "Traffic result" + ) + } + types::Reason::Sleeping => {} // Do nothing + } + } +} + +proptest! { + #[test] + fn reason_mult(e in ReasonMultExercise::strat()) { + e.test(); + } +} diff --git a/crates/wiggle/tests/union.witx b/crates/wiggle/tests/union.witx new file mode 100644 index 0000000000..d76f150ddd --- /dev/null +++ b/crates/wiggle/tests/union.witx @@ -0,0 +1,31 @@ +(use "errno.witx") +(use "excuse.witx") + +;; Every worker needs a union. Organize your workplace! +;; Fight for the full product of your labor! + +(typename $reason + (union $excuse + (field $dog_ate f32) + (field $traffic s32) + (empty $sleeping))) + +(typename $reason_mut + (union $excuse + (field $dog_ate (@witx pointer f32)) + (field $traffic (@witx pointer s32)) + (empty $sleeping))) + +(module $union_example + (@interface func (export "get_tag") + (param $r $reason) + (result $error $errno) + (result $t $excuse) + ) + + (@interface func (export "reason_mult") + (param $r $reason_mut) + (param $multiply_by u32) + (result $error $errno) + ) +) diff --git a/crates/wiggle/tests/wasi.rs b/crates/wiggle/tests/wasi.rs new file mode 100644 index 0000000000..6ad5612b69 --- /dev/null +++ b/crates/wiggle/tests/wasi.rs @@ -0,0 +1,342 @@ +use wiggle_runtime::{GuestBorrows, GuestError, GuestErrorType, GuestPtr}; +use wiggle_test::WasiCtx; + +wiggle::from_witx!({ + witx: ["tests/wasi.witx"], + ctx: WasiCtx, +}); + +type Result = std::result::Result; + +impl<'a> GuestErrorType<'a> for types::Errno { + type Context = WasiCtx<'a>; + + fn success() -> types::Errno { + types::Errno::Success + } + + fn from_error(e: GuestError, ctx: &Self::Context) -> types::Errno { + eprintln!("GUEST ERROR: {:?}", e); + ctx.guest_errors.borrow_mut().push(e); + types::Errno::Io + } +} + +impl<'a> crate::wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx<'a> { + fn args_get(&self, _argv: GuestPtr>, _argv_buf: GuestPtr) -> Result<()> { + unimplemented!("args_get") + } + + fn args_sizes_get(&self) -> Result<(types::Size, types::Size)> { + unimplemented!("args_sizes_get") + } + + fn environ_get( + &self, + _environ: GuestPtr>, + _environ_buf: GuestPtr, + ) -> Result<()> { + unimplemented!("environ_get") + } + + fn environ_sizes_get(&self) -> Result<(types::Size, types::Size)> { + unimplemented!("environ_sizes_get") + } + + fn clock_res_get(&self, _id: types::Clockid) -> Result { + unimplemented!("clock_res_get") + } + + fn clock_time_get( + &self, + _id: types::Clockid, + _precision: types::Timestamp, + ) -> Result { + unimplemented!("clock_time_get") + } + + fn fd_advise( + &self, + _fd: types::Fd, + _offset: types::Filesize, + _len: types::Filesize, + _advice: types::Advice, + ) -> Result<()> { + unimplemented!("fd_advise") + } + + fn fd_allocate( + &self, + _fd: types::Fd, + _offset: types::Filesize, + _len: types::Filesize, + ) -> Result<()> { + unimplemented!("fd_allocate") + } + + fn fd_close(&self, _fd: types::Fd) -> Result<()> { + unimplemented!("fd_close") + } + + fn fd_datasync(&self, _fd: types::Fd) -> Result<()> { + unimplemented!("fd_datasync") + } + + fn fd_fdstat_get(&self, _fd: types::Fd) -> Result { + unimplemented!("fd_fdstat_get") + } + + fn fd_fdstat_set_flags(&self, _fd: types::Fd, _flags: types::Fdflags) -> Result<()> { + unimplemented!("fd_fdstat_set_flags") + } + + fn fd_fdstat_set_rights( + &self, + _fd: types::Fd, + _fs_rights_base: types::Rights, + _fs_rights_inherting: types::Rights, + ) -> Result<()> { + unimplemented!("fd_fdstat_set_rights") + } + + fn fd_filestat_get(&self, _fd: types::Fd) -> Result { + unimplemented!("fd_filestat_get") + } + + fn fd_filestat_set_size(&self, _fd: types::Fd, _size: types::Filesize) -> Result<()> { + unimplemented!("fd_filestat_set_size") + } + + fn fd_filestat_set_times( + &self, + _fd: types::Fd, + _atim: types::Timestamp, + _mtim: types::Timestamp, + _fst_flags: types::Fstflags, + ) -> Result<()> { + unimplemented!("fd_filestat_set_times") + } + + fn fd_pread( + &self, + _fd: types::Fd, + iovs: &types::IovecArray<'_>, + _offset: types::Filesize, + ) -> Result { + // This is not functional code, but the type annotations demonstrate + // that we can use the wiggle API to create the datastructures we want + // for efficient implementation of this function elsewhere. + + let mut bc = GuestBorrows::new(); + let mut slices: Vec<&'_ mut [u8]> = Vec::new(); + // Mark the iov elements as borrowed, to ensure that they does not + // overlap with any of the as_raw regions. + bc.borrow_slice(&iovs).expect("borrow iovec array"); + for iov_ptr in iovs.iter() { + let iov_ptr = iov_ptr.expect("iovec element pointer is valid"); + + let iov: types::Iovec = iov_ptr.read().expect("read iovec element"); + let base: GuestPtr = iov.buf; + let len: u32 = iov.buf_len; + let buf: GuestPtr<[u8]> = base.as_array(len); + let slice = buf.as_raw(&mut bc).expect("borrow slice from iovec"); + slices.push(unsafe { &mut *slice }); + } + println!("iovec slices: {:?}", slices); + unimplemented!("fd_pread") + } + + fn fd_prestat_get(&self, _fd: types::Fd) -> Result { + unimplemented!("fd_prestat_get") + } + + fn fd_prestat_dir_name( + &self, + _fd: types::Fd, + _path: GuestPtr, + _path_len: types::Size, + ) -> Result<()> { + unimplemented!("fd_prestat_dir_name") + } + + fn fd_pwrite( + &self, + _fd: types::Fd, + _ciovs: &types::CiovecArray<'_>, + _offset: types::Filesize, + ) -> Result { + unimplemented!("fd_pwrite") + } + + fn fd_read(&self, _fd: types::Fd, _iovs: &types::IovecArray<'_>) -> Result { + unimplemented!("fd_read") + } + + fn fd_readdir( + &self, + _fd: types::Fd, + _buf: GuestPtr, + _buf_len: types::Size, + _cookie: types::Dircookie, + ) -> Result { + unimplemented!("fd_readdir") + } + + fn fd_renumber(&self, _fd: types::Fd, _to: types::Fd) -> Result<()> { + unimplemented!("fd_renumber") + } + + fn fd_seek( + &self, + _fd: types::Fd, + _offset: types::Filedelta, + _whence: types::Whence, + ) -> Result { + unimplemented!("fd_seek") + } + + fn fd_sync(&self, _fd: types::Fd) -> Result<()> { + unimplemented!("fd_sync") + } + + fn fd_tell(&self, _fd: types::Fd) -> Result { + unimplemented!("fd_tell") + } + + fn fd_write(&self, _fd: types::Fd, _ciovs: &types::CiovecArray<'_>) -> Result { + unimplemented!("fd_write") + } + + fn path_create_directory(&self, _fd: types::Fd, _path: &GuestPtr<'_, str>) -> Result<()> { + unimplemented!("path_create_directory") + } + + fn path_filestat_get( + &self, + _fd: types::Fd, + _flags: types::Lookupflags, + _path: &GuestPtr<'_, str>, + ) -> Result { + unimplemented!("path_filestat_get") + } + + fn path_filestat_set_times( + &self, + _fd: types::Fd, + _flags: types::Lookupflags, + _path: &GuestPtr<'_, str>, + _atim: types::Timestamp, + _mtim: types::Timestamp, + _fst_flags: types::Fstflags, + ) -> Result<()> { + unimplemented!("path_filestat_set_times") + } + + fn path_link( + &self, + _old_fd: types::Fd, + _old_flags: types::Lookupflags, + _old_path: &GuestPtr<'_, str>, + _new_fd: types::Fd, + _new_path: &GuestPtr<'_, str>, + ) -> Result<()> { + unimplemented!("path_link") + } + + fn path_open( + &self, + _fd: types::Fd, + _dirflags: types::Lookupflags, + _path: &GuestPtr<'_, str>, + _oflags: types::Oflags, + _fs_rights_base: types::Rights, + _fs_rights_inherting: types::Rights, + _fdflags: types::Fdflags, + ) -> Result { + unimplemented!("path_open") + } + + fn path_readlink( + &self, + _fd: types::Fd, + _path: &GuestPtr<'_, str>, + _buf: GuestPtr, + _buf_len: types::Size, + ) -> Result { + unimplemented!("path_readlink") + } + + fn path_remove_directory(&self, _fd: types::Fd, _path: &GuestPtr<'_, str>) -> Result<()> { + unimplemented!("path_remove_directory") + } + + fn path_rename( + &self, + _fd: types::Fd, + _old_path: &GuestPtr<'_, str>, + _new_fd: types::Fd, + _new_path: &GuestPtr<'_, str>, + ) -> Result<()> { + unimplemented!("path_rename") + } + + fn path_symlink( + &self, + _old_path: &GuestPtr<'_, str>, + _fd: types::Fd, + _new_path: &GuestPtr<'_, str>, + ) -> Result<()> { + unimplemented!("path_symlink") + } + + fn path_unlink_file(&self, _fd: types::Fd, _path: &GuestPtr<'_, str>) -> Result<()> { + unimplemented!("path_unlink_file") + } + + fn poll_oneoff( + &self, + _in_: GuestPtr, + _out: GuestPtr, + _nsubscriptions: types::Size, + ) -> Result { + unimplemented!("poll_oneoff") + } + + fn proc_exit(&self, _rval: types::Exitcode) -> std::result::Result<(), ()> { + unimplemented!("proc_exit") + } + + fn proc_raise(&self, _sig: types::Signal) -> Result<()> { + unimplemented!("proc_raise") + } + + fn sched_yield(&self) -> Result<()> { + unimplemented!("sched_yield") + } + + fn random_get(&self, _buf: GuestPtr, _buf_len: types::Size) -> Result<()> { + unimplemented!("random_get") + } + + fn sock_recv( + &self, + _fd: types::Fd, + _ri_data: &types::IovecArray<'_>, + _ri_flags: types::Riflags, + ) -> Result<(types::Size, types::Roflags)> { + unimplemented!("sock_recv") + } + + fn sock_send( + &self, + _fd: types::Fd, + _si_data: &types::CiovecArray<'_>, + _si_flags: types::Siflags, + ) -> Result { + unimplemented!("sock_send") + } + + fn sock_shutdown(&self, _fd: types::Fd, _how: types::Sdflags) -> Result<()> { + unimplemented!("sock_shutdown") + } +} diff --git a/crates/wiggle/tests/wasi.witx b/crates/wiggle/tests/wasi.witx new file mode 100644 index 0000000000..98cd947878 --- /dev/null +++ b/crates/wiggle/tests/wasi.witx @@ -0,0 +1,532 @@ +;; WASI Preview. This is an evolution of the API that WASI initially +;; launched with. +;; +;; Some content here is derived from [CloudABI](https://github.com/NuxiNL/cloudabi). +;; +;; This is a `witx` file. See [here](https://github.com/WebAssembly/WASI/tree/master/docs/witx.md) +;; for an explanation of what that means. + +(use "typenames.witx") + +(module $wasi_snapshot_preview1 + ;;; Linear memory to be accessed by WASI functions that need it. + (import "memory" (memory)) + + ;;; Read command-line argument data. + ;;; The size of the array should match that returned by `args_sizes_get` + (@interface func (export "args_get") + (param $argv (@witx pointer (@witx pointer u8))) + (param $argv_buf (@witx pointer u8)) + (result $error $errno) + ) + ;;; Return command-line argument data sizes. + (@interface func (export "args_sizes_get") + (result $error $errno) + ;;; The number of arguments. + (result $argc $size) + ;;; The size of the argument string data. + (result $argv_buf_size $size) + ) + + ;;; Read environment variable data. + ;;; The sizes of the buffers should match that returned by `environ_sizes_get`. + (@interface func (export "environ_get") + (param $environ (@witx pointer (@witx pointer u8))) + (param $environ_buf (@witx pointer u8)) + (result $error $errno) + ) + ;;; Return environment variable data sizes. + (@interface func (export "environ_sizes_get") + (result $error $errno) + ;;; The number of environment variable arguments. + (result $environc $size) + ;;; The size of the environment variable data. + (result $environ_buf_size $size) + ) + + ;;; Return the resolution of a clock. + ;;; Implementations are required to provide a non-zero value for supported clocks. For unsupported clocks, + ;;; return `errno::inval`. + ;;; Note: This is similar to `clock_getres` in POSIX. + (@interface func (export "clock_res_get") + ;;; The clock for which to return the resolution. + (param $id $clockid) + (result $error $errno) + ;;; The resolution of the clock. + (result $resolution $timestamp) + ) + ;;; Return the time value of a clock. + ;;; Note: This is similar to `clock_gettime` in POSIX. + (@interface func (export "clock_time_get") + ;;; The clock for which to return the time. + (param $id $clockid) + ;;; The maximum lag (exclusive) that the returned time value may have, compared to its actual value. + (param $precision $timestamp) + (result $error $errno) + ;;; The time value of the clock. + (result $time $timestamp) + ) + + ;;; Provide file advisory information on a file descriptor. + ;;; Note: This is similar to `posix_fadvise` in POSIX. + (@interface func (export "fd_advise") + (param $fd $fd) + ;;; The offset within the file to which the advisory applies. + (param $offset $filesize) + ;;; The length of the region to which the advisory applies. + (param $len $filesize) + ;;; The advice. + (param $advice $advice) + (result $error $errno) + ) + + ;;; Force the allocation of space in a file. + ;;; Note: This is similar to `posix_fallocate` in POSIX. + (@interface func (export "fd_allocate") + (param $fd $fd) + ;;; The offset at which to start the allocation. + (param $offset $filesize) + ;;; The length of the area that is allocated. + (param $len $filesize) + (result $error $errno) + ) + + ;;; Close a file descriptor. + ;;; Note: This is similar to `close` in POSIX. + (@interface func (export "fd_close") + (param $fd $fd) + (result $error $errno) + ) + + ;;; Synchronize the data of a file to disk. + ;;; Note: This is similar to `fdatasync` in POSIX. + (@interface func (export "fd_datasync") + (param $fd $fd) + (result $error $errno) + ) + + ;;; Get the attributes of a file descriptor. + ;;; Note: This returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as well as additional fields. + (@interface func (export "fd_fdstat_get") + (param $fd $fd) + (result $error $errno) + ;;; The buffer where the file descriptor's attributes are stored. + (result $stat $fdstat) + ) + + ;;; Adjust the flags associated with a file descriptor. + ;;; Note: This is similar to `fcntl(fd, F_SETFL, flags)` in POSIX. + (@interface func (export "fd_fdstat_set_flags") + (param $fd $fd) + ;;; The desired values of the file descriptor flags. + (param $flags $fdflags) + (result $error $errno) + ) + + ;;; Adjust the rights associated with a file descriptor. + ;;; This can only be used to remove rights, and returns `errno::notcapable` if called in a way that would attempt to add rights + (@interface func (export "fd_fdstat_set_rights") + (param $fd $fd) + ;;; The desired rights of the file descriptor. + (param $fs_rights_base $rights) + (param $fs_rights_inheriting $rights) + (result $error $errno) + ) + + ;;; Return the attributes of an open file. + (@interface func (export "fd_filestat_get") + (param $fd $fd) + (result $error $errno) + ;;; The buffer where the file's attributes are stored. + (result $buf $filestat) + ) + + ;;; Adjust the size of an open file. If this increases the file's size, the extra bytes are filled with zeros. + ;;; Note: This is similar to `ftruncate` in POSIX. + (@interface func (export "fd_filestat_set_size") + (param $fd $fd) + ;;; The desired file size. + (param $size $filesize) + (result $error $errno) + ) + + ;;; Adjust the timestamps of an open file or directory. + ;;; Note: This is similar to `futimens` in POSIX. + (@interface func (export "fd_filestat_set_times") + (param $fd $fd) + ;;; The desired values of the data access timestamp. + (param $atim $timestamp) + ;;; The desired values of the data modification timestamp. + (param $mtim $timestamp) + ;;; A bitmask indicating which timestamps to adjust. + (param $fst_flags $fstflags) + (result $error $errno) + ) + + ;;; Read from a file descriptor, without using and updating the file descriptor's offset. + ;;; Note: This is similar to `preadv` in POSIX. + (@interface func (export "fd_pread") + (param $fd $fd) + ;;; List of scatter/gather vectors in which to store data. + (param $iovs $iovec_array) + ;;; The offset within the file at which to read. + (param $offset $filesize) + (result $error $errno) + ;;; The number of bytes read. + (result $nread $size) + ) + + ;;; Return a description of the given preopened file descriptor. + (@interface func (export "fd_prestat_get") + (param $fd $fd) + (result $error $errno) + ;;; The buffer where the description is stored. + (result $buf $prestat) + ) + + ;;; Return a description of the given preopened file descriptor. + (@interface func (export "fd_prestat_dir_name") + (param $fd $fd) + ;;; A buffer into which to write the preopened directory name. + (param $path (@witx pointer u8)) + (param $path_len $size) + (result $error $errno) + ) + + ;;; Write to a file descriptor, without using and updating the file descriptor's offset. + ;;; Note: This is similar to `pwritev` in POSIX. + (@interface func (export "fd_pwrite") + (param $fd $fd) + ;;; List of scatter/gather vectors from which to retrieve data. + (param $iovs $ciovec_array) + ;;; The offset within the file at which to write. + (param $offset $filesize) + (result $error $errno) + ;;; The number of bytes written. + (result $nwritten $size) + ) + + ;;; Read from a file descriptor. + ;;; Note: This is similar to `readv` in POSIX. + (@interface func (export "fd_read") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to store data. + (param $iovs $iovec_array) + (result $error $errno) + ;;; The number of bytes read. + (result $nread $size) + ) + + ;;; Read directory entries from a directory. + ;;; When successful, the contents of the output buffer consist of a sequence of + ;;; directory entries. Each directory entry consists of a dirent_t object, + ;;; followed by dirent_t::d_namlen bytes holding the name of the directory + ;;; entry. + ;; + ;;; This function fills the output buffer as much as possible, potentially + ;;; truncating the last directory entry. This allows the caller to grow its + ;;; read buffer size in case it's too small to fit a single large directory + ;;; entry, or skip the oversized directory entry. + (@interface func (export "fd_readdir") + (param $fd $fd) + ;;; The buffer where directory entries are stored + (param $buf (@witx pointer u8)) + (param $buf_len $size) + ;;; The location within the directory to start reading + (param $cookie $dircookie) + (result $error $errno) + ;;; The number of bytes stored in the read buffer. If less than the size of the read buffer, the end of the directory has been reached. + (result $bufused $size) + ) + + ;;; Atomically replace a file descriptor by renumbering another file descriptor. + ;; + ;;; Due to the strong focus on thread safety, this environment does not provide + ;;; a mechanism to duplicate or renumber a file descriptor to an arbitrary + ;;; number, like `dup2()`. This would be prone to race conditions, as an actual + ;;; file descriptor with the same number could be allocated by a different + ;;; thread at the same time. + ;; + ;;; This function provides a way to atomically renumber file descriptors, which + ;;; would disappear if `dup2()` were to be removed entirely. + (@interface func (export "fd_renumber") + (param $fd $fd) + ;;; The file descriptor to overwrite. + (param $to $fd) + (result $error $errno) + ) + + ;;; Move the offset of a file descriptor. + ;;; Note: This is similar to `lseek` in POSIX. + (@interface func (export "fd_seek") + (param $fd $fd) + ;;; The number of bytes to move. + (param $offset $filedelta) + ;;; The base from which the offset is relative. + (param $whence $whence) + (result $error $errno) + ;;; The new offset of the file descriptor, relative to the start of the file. + (result $newoffset $filesize) + ) + + ;;; Synchronize the data and metadata of a file to disk. + ;;; Note: This is similar to `fsync` in POSIX. + (@interface func (export "fd_sync") + (param $fd $fd) + (result $error $errno) + ) + + ;;; Return the current offset of a file descriptor. + ;;; Note: This is similar to `lseek(fd, 0, SEEK_CUR)` in POSIX. + (@interface func (export "fd_tell") + (param $fd $fd) + (result $error $errno) + ;;; The current offset of the file descriptor, relative to the start of the file. + (result $offset $filesize) + ) + + ;;; Write to a file descriptor. + ;;; Note: This is similar to `writev` in POSIX. + (@interface func (export "fd_write") + (param $fd $fd) + ;;; List of scatter/gather vectors from which to retrieve data. + (param $iovs $ciovec_array) + (result $error $errno) + ;;; The number of bytes written. + (result $nwritten $size) + ) + + ;;; Create a directory. + ;;; Note: This is similar to `mkdirat` in POSIX. + (@interface func (export "path_create_directory") + (param $fd $fd) + ;;; The path at which to create the directory. + (param $path string) + (result $error $errno) + ) + + ;;; Return the attributes of a file or directory. + ;;; Note: This is similar to `stat` in POSIX. + (@interface func (export "path_filestat_get") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $flags $lookupflags) + ;;; The path of the file or directory to inspect. + (param $path string) + (result $error $errno) + ;;; The buffer where the file's attributes are stored. + (result $buf $filestat) + ) + + ;;; Adjust the timestamps of a file or directory. + ;;; Note: This is similar to `utimensat` in POSIX. + (@interface func (export "path_filestat_set_times") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $flags $lookupflags) + ;;; The path of the file or directory to operate on. + (param $path string) + ;;; The desired values of the data access timestamp. + (param $atim $timestamp) + ;;; The desired values of the data modification timestamp. + (param $mtim $timestamp) + ;;; A bitmask indicating which timestamps to adjust. + (param $fst_flags $fstflags) + (result $error $errno) + ) + + ;;; Create a hard link. + ;;; Note: This is similar to `linkat` in POSIX. + (@interface func (export "path_link") + (param $old_fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $old_flags $lookupflags) + ;;; The source path from which to link. + (param $old_path string) + ;;; The working directory at which the resolution of the new path starts. + (param $new_fd $fd) + ;;; The destination path at which to create the hard link. + (param $new_path string) + (result $error $errno) + ) + + ;;; Open a file or directory. + ;; + ;;; The returned file descriptor is not guaranteed to be the lowest-numbered + ;;; file descriptor not currently open; it is randomized to prevent + ;;; applications from depending on making assumptions about indexes, since this + ;;; is error-prone in multi-threaded contexts. The returned file descriptor is + ;;; guaranteed to be less than 2**31. + ;; + ;;; Note: This is similar to `openat` in POSIX. + (@interface func (export "path_open") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $dirflags $lookupflags) + ;;; The relative path of the file or directory to open, relative to the + ;;; `path_open::fd` directory. + (param $path string) + ;;; The method by which to open the file. + (param $oflags $oflags) + ;;; The initial rights of the newly created file descriptor. The + ;;; implementation is allowed to return a file descriptor with fewer rights + ;;; than specified, if and only if those rights do not apply to the type of + ;;; file being opened. + ;; + ;;; The *base* rights are rights that will apply to operations using the file + ;;; descriptor itself, while the *inheriting* rights are rights that apply to + ;;; file descriptors derived from it. + (param $fs_rights_base $rights) + (param $fs_rights_inherting $rights) + (param $fdflags $fdflags) + (result $error $errno) + ;;; The file descriptor of the file that has been opened. + (result $opened_fd $fd) + ) + + ;;; Read the contents of a symbolic link. + ;;; Note: This is similar to `readlinkat` in POSIX. + (@interface func (export "path_readlink") + (param $fd $fd) + ;;; The path of the symbolic link from which to read. + (param $path string) + ;;; The buffer to which to write the contents of the symbolic link. + (param $buf (@witx pointer u8)) + (param $buf_len $size) + (result $error $errno) + ;;; The number of bytes placed in the buffer. + (result $bufused $size) + ) + + ;;; Remove a directory. + ;;; Return `errno::notempty` if the directory is not empty. + ;;; Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + (@interface func (export "path_remove_directory") + (param $fd $fd) + ;;; The path to a directory to remove. + (param $path string) + (result $error $errno) + ) + + ;;; Rename a file or directory. + ;;; Note: This is similar to `renameat` in POSIX. + (@interface func (export "path_rename") + (param $fd $fd) + ;;; The source path of the file or directory to rename. + (param $old_path string) + ;;; The working directory at which the resolution of the new path starts. + (param $new_fd $fd) + ;;; The destination path to which to rename the file or directory. + (param $new_path string) + (result $error $errno) + ) + + ;;; Create a symbolic link. + ;;; Note: This is similar to `symlinkat` in POSIX. + (@interface func (export "path_symlink") + ;;; The contents of the symbolic link. + (param $old_path string) + (param $fd $fd) + ;;; The destination path at which to create the symbolic link. + (param $new_path string) + (result $error $errno) + ) + + + ;;; Unlink a file. + ;;; Return `errno::isdir` if the path refers to a directory. + ;;; Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + (@interface func (export "path_unlink_file") + (param $fd $fd) + ;;; The path to a file to unlink. + (param $path string) + (result $error $errno) + ) + + ;;; Concurrently poll for the occurrence of a set of events. + (@interface func (export "poll_oneoff") + ;;; The events to which to subscribe. + (param $in (@witx const_pointer $subscription)) + ;;; The events that have occurred. + (param $out (@witx pointer $event)) + ;;; Both the number of subscriptions and events. + (param $nsubscriptions $size) + (result $error $errno) + ;;; The number of events stored. + (result $nevents $size) + ) + + ;;; Terminate the process normally. An exit code of 0 indicates successful + ;;; termination of the program. The meanings of other values is dependent on + ;;; the environment. + (@interface func (export "proc_exit") + ;;; The exit code returned by the process. + (param $rval $exitcode) + ) + + ;;; Send a signal to the process of the calling thread. + ;;; Note: This is similar to `raise` in POSIX. + (@interface func (export "proc_raise") + ;;; The signal condition to trigger. + (param $sig $signal) + (result $error $errno) + ) + + ;;; Temporarily yield execution of the calling thread. + ;;; Note: This is similar to `sched_yield` in POSIX. + (@interface func (export "sched_yield") + (result $error $errno) + ) + + ;;; Write high-quality random data into a buffer. + ;;; This function blocks when the implementation is unable to immediately + ;;; provide sufficient high-quality random data. + ;;; This function may execute slowly, so when large mounts of random data are + ;;; required, it's advisable to use this function to seed a pseudo-random + ;;; number generator, rather than to provide the random data directly. + (@interface func (export "random_get") + ;;; The buffer to fill with random data. + (param $buf (@witx pointer u8)) + (param $buf_len $size) + (result $error $errno) + ) + + ;;; Receive a message from a socket. + ;;; Note: This is similar to `recv` in POSIX, though it also supports reading + ;;; the data into multiple buffers in the manner of `readv`. + (@interface func (export "sock_recv") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to store data. + (param $ri_data $iovec_array) + ;;; Message flags. + (param $ri_flags $riflags) + (result $error $errno) + ;;; Number of bytes stored in ri_data. + (result $ro_datalen $size) + ;;; Message flags. + (result $ro_flags $roflags) + ) + + ;;; Send a message on a socket. + ;;; Note: This is similar to `send` in POSIX, though it also supports writing + ;;; the data from multiple buffers in the manner of `writev`. + (@interface func (export "sock_send") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to retrieve data + (param $si_data $ciovec_array) + ;;; Message flags. + (param $si_flags $siflags) + (result $error $errno) + ;;; Number of bytes transmitted. + (result $so_datalen $size) + ) + + ;;; Shut down socket send and receive channels. + ;;; Note: This is similar to `shutdown` in POSIX. + (@interface func (export "sock_shutdown") + (param $fd $fd) + ;;; Which channels on the socket to shut down. + (param $how $sdflags) + (result $error $errno) + ) +)