Skip to content

Commit

Permalink
Add wit_import attribute macro to import functions from Wasm modules (
Browse files Browse the repository at this point in the history
#944)

* Add missing lift and lowering round-trip tests

Missing from some of the unit tests.

* Refactor `repeat_macro!` to support lone elements

Don't require a name and type pair, allowing macros to be applied on
lists of either just names or just types as well.

* Create a `FlatHostParameters` helper trait

Make it easier to convert a host parameters type into the flat type
parameters for an imported guest function.

* Create a `FlatHostResults` helper trait

Allow converting from the results received from an imported guest Wasm
function into the host results type.

* Create an `ImportedFunctionInterface` helper trait

Use the `FlatHostParameters` and `FlatHostResults` traits in a single
trait that can be used to determine the interface to an imported
function from a guest based on the host inteface types.

* Extract namespace from attribute parameters

Prepare to add support for a new `wit_import` attribute procedural
macro. The attribute requires receiving an extra parameter to determine
the package of the imported functions, and the namespace can also be
specified as a parameter.

Implement extraction of the namespace by parsing the attribute
parameters. It will expect the package to be specified, and will use the
namespace if provided as a parameter or fallback to the trait name if
it's not.

* Collect information for functions to import

Parse the functions from a trait and extract the information to later be
used to generate code to import functions.

* Create a `TokensSetItem` helper type

A type to allow `TokenStream`s to be used as an element in a `HashSet`.

* Generate code to import functions from guests

Given a trait interface, generate the code for a type with equivalent
methods that load a function from the guest and calls it.

* Export `InstanceWithFunction` trait

Allow generated code to use it.

* Export `Instance` trait

Allow generated code to use it.

* Refactor to use a trait alias for instances

Generate a trait alias for the instance constraints for the
implementation of the imported functions. Use this trait alias in the
generated code as well instead of using the constraints directly.

* Rename `FakeInstance` to `MockInstance`

And `FakeRuntime` to `MockRuntime`. Prepare to allow mocking exported
functions in the mocked instance.

* Allow adding mocked exported functions to instance

Keep track of the mock exported functions and allow handlers to be
called when the exported functions are called.

* Use `String` as the export type

Prepare to keep track of which handler should be called based on the
exported function name.

* Register canonical ABI functions an mocks

Prepare to use the same mechanism for the canonical ABI memory
functions.

* Allow mock exported functions to be called

Implement `InstanceWithFunction` for all valid parameters and results
types.

* Create a `MockExportedFunction` helper type

Improve the ergonomics of mocking an exported function and checking that
it's called correctly.

* Test importing a simple function

Check that a function without parameters or return types can be imported
from a mock instance.

* Test importing functions that return values

Check that functions that have return types can be imported from a mock
instance.

* Test importing functions with single parameters

Check that functions that have single parameters can be imported from a
mock instance.

* Test importing input/output functions

Check that functions that have multiple parameters and return values can
be imported from a mock instance.
  • Loading branch information
jvff authored Aug 10, 2023
1 parent dd51ecc commit 15bddb1
Show file tree
Hide file tree
Showing 18 changed files with 1,614 additions and 86 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions linera-witty-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ edition = "2021"
proc-macro = true

[dependencies]
heck = { workspace = true }
proc-macro-error = { workspace = true }
proc-macro2 = { workspace = true }
quote = { workspace = true }
Expand Down
17 changes: 16 additions & 1 deletion linera-witty-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
#![deny(missing_docs)]

mod util;
mod wit_import;
mod wit_load;
mod wit_store;
mod wit_type;

use self::util::extract_namespace;
use proc_macro::TokenStream;
use proc_macro2::Span;
use proc_macro_error::{abort, proc_macro_error};
use quote::{quote, ToTokens};
use syn::{parse_macro_input, Data, DeriveInput, Ident};
use syn::{parse_macro_input, Data, DeriveInput, Ident, ItemTrait};

/// Derives `WitType` for a Rust type.
///
Expand Down Expand Up @@ -91,3 +93,16 @@ fn derive_trait(input: DeriveInput, body: impl ToTokens, trait_name: Ident) -> T
}
.into()
}

/// Generates a generic type from a trait.
///
/// The generic type has a type parameter for the Wasm guest instance to use, and allows calling
/// functions that the instance exports through the trait's methods.
#[proc_macro_error]
#[proc_macro_attribute]
pub fn wit_import(attribute: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemTrait);
let namespace = extract_namespace(attribute, &input.ident);

wit_import::generate(input, &namespace).into()
}
115 changes: 111 additions & 4 deletions linera-witty-macros/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
// Copyright (c) Zefchain Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Helper functions shared between different macro implementations.
//! Helper types and functions shared between different macro implementations.
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Fields, Ident};
use heck::ToKebabCase;
use proc_macro2::{Span, TokenStream};
use proc_macro_error::abort;
use quote::{quote, ToTokens};
use std::hash::{Hash, Hasher};
use syn::{
parse::{self, Parse, ParseStream},
punctuated::Punctuated,
Fields, Ident, Lit, LitStr, MetaNameValue, Token,
};

/// Returns the code with a pattern to match a heterogenous list using the `field_names` as
/// bindings.
Expand All @@ -22,3 +29,103 @@ pub fn hlist_type_for(fields: &Fields) -> TokenStream {
let field_types = fields.iter().map(|field| &field.ty);
quote! { linera_witty::HList![#( #field_types ),*] }
}

/// Returns the package and namespace for the WIT interface generated by an attribute macro.
///
/// Requires a `package` to be specified in `attribute_parameters` and can use a specified
/// `namespace` or infer it from the `type_name`.
pub fn extract_namespace(
attribute_parameters: proc_macro::TokenStream,
type_name: &Ident,
) -> LitStr {
let span = Span::call_site();
let parameters = syn::parse::<AttributeParameters>(attribute_parameters).unwrap_or_else(|_| {
abort!(
span,
r#"Failed to parse attribute parameters, expected either `root = true` \
or `package = "namespace:package""#
)
});

let package_name = parameters.parameter("package").unwrap_or_else(|| {
abort!(
span,
r#"Missing package name specifier in attribute parameters \
(package = "namespace:package")"#
)
});

let interface_name = parameters
.parameter("interface")
.unwrap_or_else(|| type_name.to_string().to_kebab_case());

LitStr::new(&format!("{package_name}/{interface_name}"), span)
}

/// A type representing the parameters for an attribute procedural macro.
struct AttributeParameters {
metadata: Punctuated<MetaNameValue, Token![,]>,
}

impl Parse for AttributeParameters {
fn parse(input: ParseStream) -> parse::Result<Self> {
Ok(AttributeParameters {
metadata: Punctuated::parse_terminated(input)?,
})
}
}

impl AttributeParameters {
/// Returns the string value of a parameter named `name`, if it exists.
pub fn parameter(&self, name: &str) -> Option<String> {
self.metadata
.iter()
.find(|pair| pair.path.is_ident(name))
.map(|pair| {
let Lit::Str(lit_str) = &pair.lit else {
abort!(&pair.lit, "Expected a string literal");
};

lit_str.value()
})
}
}

/// A helper type to allow comparing [`TokenStream`] instances, allowing it to be used in a
/// [`HashSet`].
pub struct TokensSetItem<'input> {
string: String,
tokens: &'input TokenStream,
}

impl<'input> From<&'input TokenStream> for TokensSetItem<'input> {
fn from(tokens: &'input TokenStream) -> Self {
TokensSetItem {
string: tokens.to_string(),
tokens,
}
}
}

impl PartialEq for TokensSetItem<'_> {
fn eq(&self, other: &Self) -> bool {
self.string.eq(&other.string)
}
}

impl Eq for TokensSetItem<'_> {}

impl Hash for TokensSetItem<'_> {
fn hash<H>(&self, state: &mut H)
where
H: Hasher,
{
self.string.hash(state)
}
}

impl ToTokens for TokensSetItem<'_> {
fn to_tokens(&self, stream: &mut TokenStream) {
self.tokens.to_tokens(stream)
}
}
Loading

0 comments on commit 15bddb1

Please sign in to comment.