Skip to content

Commit

Permalink
Merge pull request #678 from tessi/feature/using-component
Browse files Browse the repository at this point in the history
Feature/using component
  • Loading branch information
superchris authored Dec 18, 2024
2 parents 1351e6d + 3a627bd commit bdbd1fe
Show file tree
Hide file tree
Showing 16 changed files with 221 additions and 6 deletions.
44 changes: 44 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,50 @@
"kind": "build",
"isDefault": true
}
},
{
"label": "rust clippy",
"type": "process",
"command": "cargo",
"args": [
"clippy",
"--all-targets",
"--all-features",
"--",
"-D",
"warnings",
"-A",
"clippy::extra_unused_lifetimes"
],
"options": {
"cwd": "${workspaceRoot}/native/wasmex"
},
"problemMatcher": [
"$mixCompileWarning",
"$mixCompileError"
],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "rust format",
"type": "process",
"command": "cargo",
"args": ["fmt"],
"options": {
"cwd": "${workspaceRoot}/native/wasmex"
},
"problemMatcher": [
"$mixCompileWarning",
"$mixCompileError"
],
"group": {
"kind": "build",
"isDefault": true
}
}

]
}
36 changes: 36 additions & 0 deletions lib/wasmex/components/component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,40 @@ defmodule Wasmex.Components.Component do
resource -> {:ok, __wrap_resource__(resource)}
end
end

defmacro __using__(opts) do
genserver_setup =
quote do
use GenServer

def start_link(opts) do
Wasmex.Components.start_link(opts)
end

def handle_call(request, from, state) do
Wasmex.Components.handle_call(request, from, state)
end
end

functions =
if wit_path = Keyword.get(opts, :wit) do
wit_contents = File.read!(wit_path)
exported_functions = Wasmex.Native.wit_exported_functions(wit_path, wit_contents)

for {function, arity} <- exported_functions do
arglist = Macro.generate_arguments(arity, __MODULE__)
function_atom = function |> String.replace("-", "_") |> String.to_atom()

quote do
def unquote(function_atom)(pid, unquote_splicing(arglist)) do
Wasmex.Components.call_function(pid, unquote(function), [unquote_splicing(arglist)])
end
end
end
else
[]
end

[genserver_setup, functions]
end
end
2 changes: 2 additions & 0 deletions lib/wasmex/native.ex
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ defmodule Wasmex.Native do

def component_call_function(_store, _instance, _function_name, _params), do: error()

def wit_exported_functions(_path, _wit), do: error()

# When the NIF is loaded, it will override functions in this module.
# Calling error is handles the case when the nif could not be loaded.
defp error, do: :erlang.nif_error(:nif_not_loaded)
Expand Down
50 changes: 48 additions & 2 deletions native/wasmex/Cargo.lock

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

2 changes: 2 additions & 0 deletions native/wasmex/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ wasmtime-wasi-http = "26.0.1"
wasi-common = "26.0.1"
wiggle = "26.0.1"
wat = "1.220.0"
wit-parser = "0.221.2"
convert_case = "0.6.0"
12 changes: 8 additions & 4 deletions native/wasmex/src/component_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use crate::component::ComponentInstanceResource;
use crate::store::ComponentStoreData;
use crate::store::ComponentStoreResource;

use convert_case::{Case, Casing};

use rustler::types::atom::nil;
use rustler::types::tuple;
use rustler::types::tuple::make_tuple;
Expand Down Expand Up @@ -63,7 +65,9 @@ pub fn component_call_function<'a>(
let _ = function.post_return(&mut *component_store);
Ok(encode_result(env, result))
}
Err(err) => Ok(env.error_tuple(format!("Error executing function: {err}"))),
Err(err) => Err(rustler::Error::Term(Box::new(format!(
"Error executing function: {err}"
)))),
}
}

Expand Down Expand Up @@ -150,13 +154,13 @@ fn term_to_val(param_term: &Term, param_type: &Type) -> Result<Val, Error> {

fn term_to_field_name(key_term: &Term) -> String {
match key_term.get_type() {
TermType::Atom => key_term.atom_to_string().unwrap(),
_ => key_term.decode::<String>().unwrap(),
TermType::Atom => key_term.atom_to_string().unwrap().to_case(Case::Kebab),
_ => key_term.decode::<String>().unwrap().to_case(Case::Kebab),
}
}

fn field_name_to_term<'a>(env: &rustler::Env<'a>, field_name: &str) -> Term<'a> {
rustler::serde::atoms::str_to_term(env, field_name).unwrap()
rustler::serde::atoms::str_to_term(env, &field_name.to_case(Case::Snake)).unwrap()
}

fn encode_result(env: rustler::Env, vals: Vec<Val>) -> Term {
Expand Down
1 change: 1 addition & 0 deletions native/wasmex/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ pub mod module;
pub mod pipe;
pub mod printable_term_type;
pub mod store;
pub mod wit;

rustler::init!("Elixir.Wasmex.Native");
22 changes: 22 additions & 0 deletions native/wasmex/src/wit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use rustler::{NifResult, Term};
use wit_parser::{Resolve, WorldItem};

#[rustler::nif(name = "wit_exported_functions")]
pub fn exported_functions(env: rustler::Env, path: String, wit: String) -> NifResult<Term> {
let mut resolve = Resolve::new();
let id = resolve
.push_str(path, &wit)
.map_err(|e| rustler::Error::Term(Box::new(format!("Failed to parse WIT: {}", e))))?;
let world_id = resolve
.select_world(id, None)
.map_err(|e| rustler::Error::Term(Box::new(format!("Failed to select world: {}", e))))?;
let exports = &resolve.worlds[world_id].exports;
let exported_functions = exports
.iter()
.filter_map(|(_key, value)| match value {
WorldItem::Function(function) => Some((&function.name, function.params.len())),
_ => None,
})
.collect::<Vec<(&String, usize)>>();
Ok(Term::map_from_pairs(env, exported_functions.as_slice()).unwrap())
}
5 changes: 5 additions & 0 deletions test/component_fixtures/hello_world.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule HelloWorld do
@moduledoc false

use Wasmex.Components.Component, wit: "test/component_fixtures/hello_world/hello-world.wit"
end
1 change: 1 addition & 0 deletions test/component_fixtures/hello_world/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx jco componentize -w hello-world.wit -o hello_world.wasm hello-world.js
4 changes: 4 additions & 0 deletions test/component_fixtures/hello_world/hello-world.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ export function multiGreet(who, times) {

export function greetMany(people) {
return people.map((person) => `Hello, ${person}!`);
}

export function echoKebab(kebabRecord) {
return kebabRecord;
}
4 changes: 4 additions & 0 deletions test/component_fixtures/hello_world/hello-world.wit
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package local:hello-world;

world hello-world {
record kebab-record {
kebab-field: string
}
export greet: func(who: string) -> string;
export multi-greet: func(who: string, times: u16) -> list<string>;
export greet-many: func(people: list<string>) -> list<string>;
export echo-kebab: func(kebab-record: kebab-record) -> kebab-record;
}
Binary file modified test/component_fixtures/hello_world/hello_world.wasm
Binary file not shown.
14 changes: 14 additions & 0 deletions test/components/component_types_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Wasm.Components.ComponentTypesTest do
use ExUnit.Case, async: true

alias Wasmex.Wasi.WasiP2Options

setup do
{:ok, store} = Wasmex.Components.Store.new()
component_bytes = File.read!("test/component_fixtures/component_types/component_types.wasm")
Expand Down Expand Up @@ -49,6 +51,18 @@ defmodule Wasm.Components.ComponentTypesTest do
])
end

test "record with kebab-field" do
{:ok, store} = Wasmex.Components.Store.new_wasi(%WasiP2Options{})
component_bytes = File.read!("test/component_fixtures/hello_world/hello_world.wasm")
{:ok, component} = Wasmex.Components.Component.new(store, component_bytes)
{:ok, instance} = Wasmex.Components.Instance.new(store, component)

assert {:ok, %{kebab_field: "foo"}} =
Wasmex.Components.Instance.call_function(instance, "echo-kebab", [
%{kebab_field: "foo"}
])
end

test "lists", %{instance: instance} do
assert {:ok, [1, 2, 3]} =
Wasmex.Components.Instance.call_function(instance, "id-list", [[1, 2, 3]])
Expand Down
12 changes: 12 additions & 0 deletions test/components/components_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ defmodule Wasmex.ComponentsTest do
assert {:error, _error} = Wasmex.Components.call_function(component_pid, "garbage", ["wut"])
end

test "using the component macro" do
component_bytes = File.read!("test/component_fixtures/hello_world/hello_world.wasm")

component_pid =
start_supervised!({HelloWorld, %{bytes: component_bytes, wasi: %WasiP2Options{}}})

assert {:ok, "Hello, Elixir!"} = HelloWorld.greet(component_pid, "Elixir")

assert {:ok, ["Hello, Elixir!", "Hello, Elixir!"]} =
HelloWorld.multi_greet(component_pid, "Elixir", 2)
end

test "wasi interaction" do
component_bytes = File.read!("test/component_fixtures/wasi_p2_test/wasi-p2-test.wasm")

Expand Down
18 changes: 18 additions & 0 deletions test/components/wit_parser_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule Components.WitParserTest do
use ExUnit.Case

describe "exports" do
test "exported_functions" do
wit = File.read!("test/component_fixtures/hello_world/hello-world.wit")

assert %{"greet" => 1, "greet-many" => 1, "multi-greet" => 2} =
Wasmex.Native.wit_exported_functions("hello-world.wit", wit)
end

test "wit parse errors" do
wit = "goo goo"
assert {:error, error} = Wasmex.Native.wit_exported_functions("hello-world.wit", wit)
assert error =~ "Failed to parse WIT"
end
end
end

0 comments on commit bdbd1fe

Please sign in to comment.