Skip to content

Commit 460066f

Browse files
authored
Merge pull request #714 from tessi/docs-and-refactoring
Docs and refactoring
2 parents acda0a0 + 23c705a commit 460066f

File tree

6 files changed

+363
-57
lines changed

6 files changed

+363
-57
lines changed

lib/wasmex/components.ex

+171-15
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,169 @@ defmodule Wasmex.Components do
22
@moduledoc """
33
This is the entry point to support for the [WebAssembly Component Model](https://component-model.bytecodealliance.org/).
44
5-
Support should be considered experimental at this point, with not all types yet supported.
5+
The Component Model is a higher-level way to interact with WebAssembly modules that provides:
6+
- Better type safety through interface types
7+
- Standardized way to define imports and exports using WIT (WebAssembly Interface Types)
8+
- WASI support for system interface capabilities
9+
10+
## Basic Usage
11+
12+
To use a WebAssembly component:
13+
14+
1. Start a component instance:
15+
```elixir
16+
# Using raw bytes
17+
bytes = File.read!("path/to/component.wasm")
18+
{:ok, pid} = Wasmex.Components.start_link(%{bytes: bytes})
19+
20+
# Using a file path
21+
{:ok, pid} = Wasmex.Components.start_link(%{path: "path/to/component.wasm"})
22+
23+
# With WASI support
24+
{:ok, pid} = Wasmex.Components.start_link(%{
25+
path: "path/to/component.wasm",
26+
wasi: %Wasmex.Wasi.WasiP2Options{}
27+
})
28+
29+
# With imports (host functions the component can call)
30+
{:ok, pid} = Wasmex.Components.start_link(%{
31+
bytes: bytes,
32+
imports: %{
33+
"host_function" => {:fn, &MyModule.host_function/1}
34+
}
35+
})
36+
```
37+
38+
2. Call exported functions:
39+
```elixir
40+
{:ok, result} = Wasmex.Components.call_function(pid, "exported_function", ["param1"])
41+
```
42+
43+
## Component Interface Types
44+
45+
The component model supports the following WIT (WebAssembly Interface Type) types:
46+
47+
### Currently Supported Types
48+
49+
- **Primitive Types**
50+
- Integers: `s8`, `s16`, `s32`, `s64`, `u8`, `u16`, `u32`, `u64`
51+
- Floats: `f32`, `f64`
52+
- `bool`
53+
- `string`
54+
55+
- **Compound Types**
56+
- `record` (maps to Elixir maps with atom keys)
57+
```wit
58+
record point { x: u32, y: u32 }
59+
```
60+
```elixir
61+
%{x: 1, y: 2}
62+
```
63+
64+
- `list<T>` (maps to Elixir lists)
65+
```wit
66+
list<u32>
67+
```
68+
```elixir
69+
[1, 2, 3]
70+
```
71+
72+
- `tuple<T1, T2>` (maps to Elixir tuples)
73+
```wit
74+
tuple<u32, string>
75+
```
76+
```elixir
77+
{1, "two"}
78+
```
79+
80+
- `option<T>` (maps to `nil` or the value)
81+
```wit
82+
option<u32>
83+
```
84+
```elixir
85+
nil # or
86+
42
87+
```
88+
89+
### Currently Unsupported Types
90+
91+
The following WIT types are not yet supported:
92+
- `char`
93+
- `variant` (tagged unions)
94+
- `enum`
95+
- `flags`
96+
- `result` types
97+
- Resources
98+
99+
Support should be considered experimental at this point.
100+
101+
## Options
102+
103+
The `start_link/1` function accepts the following options:
104+
105+
* `:bytes` - Raw WebAssembly component bytes (mutually exclusive with `:path`)
106+
* `:path` - Path to a WebAssembly component file (mutually exclusive with `:bytes`)
107+
* `:wasi` - Optional WASI configuration as `Wasmex.Wasi.WasiP2Options` struct for system interface capabilities
108+
* `:imports` - Optional map of host functions that can be called by the WebAssembly component
109+
* Keys are function names as strings
110+
* Values are tuples of `{:fn, function}` where function is the host function to call
111+
112+
Additionally, any standard GenServer options (like `:name`) are supported.
113+
114+
### Examples
115+
116+
```elixir
117+
# With raw bytes
118+
{:ok, pid} = Wasmex.Components.start_link(%{
119+
bytes: File.read!("component.wasm"),
120+
name: MyComponent
121+
})
122+
123+
# With WASI configuration
124+
{:ok, pid} = Wasmex.Components.start_link(%{
125+
path: "component.wasm",
126+
wasi: %Wasmex.Wasi.WasiP2Options{
127+
args: ["arg1", "arg2"],
128+
env: %{"KEY" => "value"},
129+
preopened_dirs: ["/tmp"]
130+
}
131+
})
132+
133+
# With host functions
134+
{:ok, pid} = Wasmex.Components.start_link(%{
135+
path: "component.wasm",
136+
imports: %{
137+
"log" => {:fn, &IO.puts/1},
138+
"add" => {:fn, fn(a, b) -> a + b end}
139+
}
140+
})
141+
```
6142
"""
7143

8144
use GenServer
9145
alias Wasmex.Wasi.WasiP2Options
10146

11-
def start_link(%{bytes: component_bytes, wasi: %WasiP2Options{} = wasi_options}) do
12-
with {:ok, store} <- Wasmex.Components.Store.new_wasi(wasi_options),
13-
{:ok, component} <- Wasmex.Components.Component.new(store, component_bytes) do
14-
GenServer.start_link(__MODULE__, %{store: store, component: component})
15-
end
16-
end
147+
@doc """
148+
Starts a new WebAssembly component instance.
17149
18-
def start_link(%{bytes: component_bytes}) do
19-
with {:ok, store} <- Wasmex.Components.Store.new(),
20-
{:ok, component} <- Wasmex.Components.Component.new(store, component_bytes) do
21-
GenServer.start_link(__MODULE__, %{store: store, component: component})
22-
end
23-
end
150+
## Options
151+
152+
* `:bytes` - Raw WebAssembly component bytes (mutually exclusive with `:path`)
153+
* `:path` - Path to a WebAssembly component file (mutually exclusive with `:bytes`)
154+
* `:wasi` - Optional WASI configuration as `Wasmex.Wasi.WasiP2Options` struct
155+
* `:imports` - Optional map of host functions that can be called by the component
156+
* Any standard GenServer options (like `:name`)
157+
158+
## Returns
159+
160+
* `{:ok, pid}` on success
161+
* `{:error, reason}` on failure
162+
"""
163+
def start_link(opts) when is_list(opts) or is_map(opts) do
164+
opts = normalize_opts(opts)
24165

25-
def start_link(opts) when is_list(opts) do
26166
with {:ok, store} <- build_store(opts),
27-
component_bytes <- Keyword.get(opts, :bytes),
167+
component_bytes <- get_component_bytes(opts),
28168
imports <- Keyword.get(opts, :imports, %{}),
29169
{:ok, component} <- Wasmex.Components.Component.new(store, component_bytes) do
30170
GenServer.start_link(
@@ -35,6 +175,22 @@ defmodule Wasmex.Components do
35175
end
36176
end
37177

178+
defp normalize_opts(opts) when is_map(opts) do
179+
opts
180+
|> Map.to_list()
181+
|> Keyword.new()
182+
end
183+
184+
defp normalize_opts(opts) when is_list(opts), do: opts
185+
186+
defp get_component_bytes(opts) do
187+
cond do
188+
bytes = Keyword.get(opts, :bytes) -> bytes
189+
path = Keyword.get(opts, :path) -> File.read!(path)
190+
true -> raise ArgumentError, "Either :bytes or :path must be provided"
191+
end
192+
end
193+
38194
defp build_store(opts) do
39195
if wasi_options = Keyword.get(opts, :wasi) do
40196
Wasmex.Components.Store.new_wasi(wasi_options)

lib/wasmex/components/component.ex

-38
Original file line numberDiff line numberDiff line change
@@ -31,42 +31,4 @@ defmodule Wasmex.Components.Component do
3131
resource -> {:ok, __wrap_resource__(resource)}
3232
end
3333
end
34-
35-
defmacro __using__(opts) do
36-
macro_imports = Keyword.get(opts, :imports, %{})
37-
38-
genserver_setup =
39-
quote do
40-
use GenServer
41-
42-
def start_link(opts) do
43-
Wasmex.Components.start_link(opts |> Keyword.put(:imports, unquote(macro_imports)))
44-
end
45-
46-
def handle_call(request, from, state) do
47-
Wasmex.Components.handle_call(request, from, state)
48-
end
49-
end
50-
51-
functions =
52-
if wit_path = Keyword.get(opts, :wit) do
53-
wit_contents = File.read!(wit_path)
54-
exported_functions = Wasmex.Native.wit_exported_functions(wit_path, wit_contents)
55-
56-
for {function, arity} <- exported_functions do
57-
arglist = Macro.generate_arguments(arity, __MODULE__)
58-
function_atom = function |> String.replace("-", "_") |> String.to_atom()
59-
60-
quote do
61-
def unquote(function_atom)(pid, unquote_splicing(arglist)) do
62-
Wasmex.Components.call_function(pid, unquote(function), [unquote_splicing(arglist)])
63-
end
64-
end
65-
end
66-
else
67-
[]
68-
end
69-
70-
[genserver_setup, functions]
71-
end
7234
end
+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
defmodule Wasmex.Components.ComponentServer do
2+
@moduledoc """
3+
A GenServer wrapper for WebAssembly components. This module provides a macro to easily
4+
create GenServer-based components with wrapper functions for the exports in the WIT definition.
5+
6+
## Usage
7+
8+
To use this module, you need to:
9+
1. Create a WIT file defining your component's interface
10+
2. Create a module that uses ComponentServer with the path to your WIT file
11+
3. Use the generated functions to interact with your WebAssembly component
12+
13+
## Basic Example
14+
15+
Given a WIT file `greeter.wit` with the following content:
16+
17+
```wit
18+
package example:greeter
19+
20+
world greeter {
21+
export greet: func(who: string) -> string;
22+
export multi-greet: func(who: string, times: u16) -> list<string>;
23+
}
24+
```
25+
26+
You can create a GenServer wrapper like this:
27+
28+
```elixir
29+
defmodule MyApp.Greeter do
30+
use Wasmex.Components.ComponentServer,
31+
wit: "path/to/greeter.wit"
32+
end
33+
```
34+
35+
This will automatically generate the following functions:
36+
37+
```elixir
38+
# Start the component server
39+
iex> {:ok, pid} = MyApp.Greeter.start_link(path: "path/to/greeter.wasm")
40+
41+
# Generated function wrappers:
42+
iex> MyApp.Greeter.greet(pid, "World") # Returns: "Hello, World!"
43+
iex> MyApp.Greeter.multi_greet(pid, "World", 2) # Returns: ["Hello, World!", "Hello, World!"]
44+
```
45+
46+
## Imports Example
47+
48+
When your WebAssembly component imports functions, you can provide them using the `:imports` option.
49+
For example, given a WIT file `logger.wit`:
50+
51+
```wit
52+
package example:logger
53+
54+
world logger {
55+
import log: func(message: string)
56+
import get-timestamp: func() -> u64
57+
58+
export log-with-timestamp: func(message: string)
59+
}
60+
```
61+
62+
You can implement the imported functions like this:
63+
64+
```elixir
65+
defmodule MyApp.Logger do
66+
use Wasmex.Components.ComponentServer,
67+
wit: "path/to/logger.wit",
68+
imports: %{
69+
"log" => fn message ->
70+
IO.puts(message)
71+
:ok
72+
end,
73+
"get-timestamp" => fn ->
74+
System.system_time(:second)
75+
end
76+
}
77+
end
78+
```
79+
80+
# Usage:
81+
```elixir
82+
iex> {:ok, pid} = MyApp.Logger.start_link(wasm: "path/to/logger.wasm")
83+
iex> MyApp.Logger.log_with_timestamp(pid, "Hello from Wasm!")
84+
```
85+
86+
The import functions should return the correct types as defined in the WIT file. Incorrect types will likely
87+
cause a crash, or possibly a NIF panic.
88+
89+
## Options
90+
91+
* `:wit` - Path to the WIT file defining the component's interface
92+
* `:imports` - A map of import function implementations that the component requires, where each key
93+
is the function name as defined in the WIT file and the value is the implementing function
94+
"""
95+
96+
defmacro __using__(opts) do
97+
macro_imports = Keyword.get(opts, :imports, %{})
98+
99+
genserver_setup =
100+
quote do
101+
use GenServer
102+
103+
def start_link(opts) do
104+
Wasmex.Components.start_link(opts |> Keyword.put(:imports, unquote(macro_imports)))
105+
end
106+
107+
def handle_call(request, from, state) do
108+
Wasmex.Components.handle_call(request, from, state)
109+
end
110+
end
111+
112+
functions =
113+
if wit_path = Keyword.get(opts, :wit) do
114+
wit_contents = File.read!(wit_path)
115+
exported_functions = Wasmex.Native.wit_exported_functions(wit_path, wit_contents)
116+
117+
for {function, arity} <- exported_functions do
118+
arglist = Macro.generate_arguments(arity, __MODULE__)
119+
function_atom = function |> String.replace("-", "_") |> String.to_atom()
120+
121+
quote do
122+
def unquote(function_atom)(pid, unquote_splicing(arglist)) do
123+
Wasmex.Components.call_function(pid, unquote(function), [unquote_splicing(arglist)])
124+
end
125+
end
126+
end
127+
else
128+
[]
129+
end
130+
131+
[genserver_setup, functions]
132+
end
133+
end

0 commit comments

Comments
 (0)