Skip to content

Commit

Permalink
Merge pull request #714 from tessi/docs-and-refactoring
Browse files Browse the repository at this point in the history
Docs and refactoring
  • Loading branch information
superchris authored Jan 26, 2025
2 parents acda0a0 + 23c705a commit 460066f
Show file tree
Hide file tree
Showing 6 changed files with 363 additions and 57 deletions.
186 changes: 171 additions & 15 deletions lib/wasmex/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,169 @@ defmodule Wasmex.Components do
@moduledoc """
This is the entry point to support for the [WebAssembly Component Model](https://component-model.bytecodealliance.org/).
Support should be considered experimental at this point, with not all types yet supported.
The Component Model is a higher-level way to interact with WebAssembly modules that provides:
- Better type safety through interface types
- Standardized way to define imports and exports using WIT (WebAssembly Interface Types)
- WASI support for system interface capabilities
## Basic Usage
To use a WebAssembly component:
1. Start a component instance:
```elixir
# Using raw bytes
bytes = File.read!("path/to/component.wasm")
{:ok, pid} = Wasmex.Components.start_link(%{bytes: bytes})
# Using a file path
{:ok, pid} = Wasmex.Components.start_link(%{path: "path/to/component.wasm"})
# With WASI support
{:ok, pid} = Wasmex.Components.start_link(%{
path: "path/to/component.wasm",
wasi: %Wasmex.Wasi.WasiP2Options{}
})
# With imports (host functions the component can call)
{:ok, pid} = Wasmex.Components.start_link(%{
bytes: bytes,
imports: %{
"host_function" => {:fn, &MyModule.host_function/1}
}
})
```
2. Call exported functions:
```elixir
{:ok, result} = Wasmex.Components.call_function(pid, "exported_function", ["param1"])
```
## Component Interface Types
The component model supports the following WIT (WebAssembly Interface Type) types:
### Currently Supported Types
- **Primitive Types**
- Integers: `s8`, `s16`, `s32`, `s64`, `u8`, `u16`, `u32`, `u64`
- Floats: `f32`, `f64`
- `bool`
- `string`
- **Compound Types**
- `record` (maps to Elixir maps with atom keys)
```wit
record point { x: u32, y: u32 }
```
```elixir
%{x: 1, y: 2}
```
- `list<T>` (maps to Elixir lists)
```wit
list<u32>
```
```elixir
[1, 2, 3]
```
- `tuple<T1, T2>` (maps to Elixir tuples)
```wit
tuple<u32, string>
```
```elixir
{1, "two"}
```
- `option<T>` (maps to `nil` or the value)
```wit
option<u32>
```
```elixir
nil # or
42
```
### Currently Unsupported Types
The following WIT types are not yet supported:
- `char`
- `variant` (tagged unions)
- `enum`
- `flags`
- `result` types
- Resources
Support should be considered experimental at this point.
## Options
The `start_link/1` function accepts the following options:
* `:bytes` - Raw WebAssembly component bytes (mutually exclusive with `:path`)
* `:path` - Path to a WebAssembly component file (mutually exclusive with `:bytes`)
* `:wasi` - Optional WASI configuration as `Wasmex.Wasi.WasiP2Options` struct for system interface capabilities
* `:imports` - Optional map of host functions that can be called by the WebAssembly component
* Keys are function names as strings
* Values are tuples of `{:fn, function}` where function is the host function to call
Additionally, any standard GenServer options (like `:name`) are supported.
### Examples
```elixir
# With raw bytes
{:ok, pid} = Wasmex.Components.start_link(%{
bytes: File.read!("component.wasm"),
name: MyComponent
})
# With WASI configuration
{:ok, pid} = Wasmex.Components.start_link(%{
path: "component.wasm",
wasi: %Wasmex.Wasi.WasiP2Options{
args: ["arg1", "arg2"],
env: %{"KEY" => "value"},
preopened_dirs: ["/tmp"]
}
})
# With host functions
{:ok, pid} = Wasmex.Components.start_link(%{
path: "component.wasm",
imports: %{
"log" => {:fn, &IO.puts/1},
"add" => {:fn, fn(a, b) -> a + b end}
}
})
```
"""

use GenServer
alias Wasmex.Wasi.WasiP2Options

Check warning on line 145 in lib/wasmex/components.ex

View workflow job for this annotation

GitHub Actions / OTP 25.2 / Elixir 1.15.8

unused alias WasiP2Options

Check warning on line 145 in lib/wasmex/components.ex

View workflow job for this annotation

GitHub Actions / OTP 26.2 / Elixir 1.15.8

unused alias WasiP2Options

def start_link(%{bytes: component_bytes, wasi: %WasiP2Options{} = wasi_options}) do
with {:ok, store} <- Wasmex.Components.Store.new_wasi(wasi_options),
{:ok, component} <- Wasmex.Components.Component.new(store, component_bytes) do
GenServer.start_link(__MODULE__, %{store: store, component: component})
end
end
@doc """
Starts a new WebAssembly component instance.
def start_link(%{bytes: component_bytes}) do
with {:ok, store} <- Wasmex.Components.Store.new(),
{:ok, component} <- Wasmex.Components.Component.new(store, component_bytes) do
GenServer.start_link(__MODULE__, %{store: store, component: component})
end
end
## Options
* `:bytes` - Raw WebAssembly component bytes (mutually exclusive with `:path`)
* `:path` - Path to a WebAssembly component file (mutually exclusive with `:bytes`)
* `:wasi` - Optional WASI configuration as `Wasmex.Wasi.WasiP2Options` struct
* `:imports` - Optional map of host functions that can be called by the component
* Any standard GenServer options (like `:name`)
## Returns
* `{:ok, pid}` on success
* `{:error, reason}` on failure
"""
def start_link(opts) when is_list(opts) or is_map(opts) do
opts = normalize_opts(opts)

def start_link(opts) when is_list(opts) do
with {:ok, store} <- build_store(opts),
component_bytes <- Keyword.get(opts, :bytes),
component_bytes <- get_component_bytes(opts),
imports <- Keyword.get(opts, :imports, %{}),
{:ok, component} <- Wasmex.Components.Component.new(store, component_bytes) do
GenServer.start_link(
Expand All @@ -35,6 +175,22 @@ defmodule Wasmex.Components do
end
end

defp normalize_opts(opts) when is_map(opts) do
opts
|> Map.to_list()
|> Keyword.new()
end

defp normalize_opts(opts) when is_list(opts), do: opts

defp get_component_bytes(opts) do
cond do
bytes = Keyword.get(opts, :bytes) -> bytes
path = Keyword.get(opts, :path) -> File.read!(path)
true -> raise ArgumentError, "Either :bytes or :path must be provided"
end
end

defp build_store(opts) do
if wasi_options = Keyword.get(opts, :wasi) do
Wasmex.Components.Store.new_wasi(wasi_options)
Expand Down
38 changes: 0 additions & 38 deletions lib/wasmex/components/component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,42 +31,4 @@ defmodule Wasmex.Components.Component do
resource -> {:ok, __wrap_resource__(resource)}
end
end

defmacro __using__(opts) do
macro_imports = Keyword.get(opts, :imports, %{})

genserver_setup =
quote do
use GenServer

def start_link(opts) do
Wasmex.Components.start_link(opts |> Keyword.put(:imports, unquote(macro_imports)))
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
133 changes: 133 additions & 0 deletions lib/wasmex/components/component_server.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
defmodule Wasmex.Components.ComponentServer do
@moduledoc """
A GenServer wrapper for WebAssembly components. This module provides a macro to easily
create GenServer-based components with wrapper functions for the exports in the WIT definition.
## Usage
To use this module, you need to:
1. Create a WIT file defining your component's interface
2. Create a module that uses ComponentServer with the path to your WIT file
3. Use the generated functions to interact with your WebAssembly component
## Basic Example
Given a WIT file `greeter.wit` with the following content:
```wit
package example:greeter
world greeter {
export greet: func(who: string) -> string;
export multi-greet: func(who: string, times: u16) -> list<string>;
}
```
You can create a GenServer wrapper like this:
```elixir
defmodule MyApp.Greeter do
use Wasmex.Components.ComponentServer,
wit: "path/to/greeter.wit"
end
```
This will automatically generate the following functions:
```elixir
# Start the component server
iex> {:ok, pid} = MyApp.Greeter.start_link(path: "path/to/greeter.wasm")
# Generated function wrappers:
iex> MyApp.Greeter.greet(pid, "World") # Returns: "Hello, World!"
iex> MyApp.Greeter.multi_greet(pid, "World", 2) # Returns: ["Hello, World!", "Hello, World!"]
```
## Imports Example
When your WebAssembly component imports functions, you can provide them using the `:imports` option.
For example, given a WIT file `logger.wit`:
```wit
package example:logger
world logger {
import log: func(message: string)
import get-timestamp: func() -> u64
export log-with-timestamp: func(message: string)
}
```
You can implement the imported functions like this:
```elixir
defmodule MyApp.Logger do
use Wasmex.Components.ComponentServer,
wit: "path/to/logger.wit",
imports: %{
"log" => fn message ->
IO.puts(message)
:ok
end,
"get-timestamp" => fn ->
System.system_time(:second)
end
}
end
```
# Usage:
```elixir
iex> {:ok, pid} = MyApp.Logger.start_link(wasm: "path/to/logger.wasm")
iex> MyApp.Logger.log_with_timestamp(pid, "Hello from Wasm!")
```
The import functions should return the correct types as defined in the WIT file. Incorrect types will likely
cause a crash, or possibly a NIF panic.
## Options
* `:wit` - Path to the WIT file defining the component's interface
* `:imports` - A map of import function implementations that the component requires, where each key
is the function name as defined in the WIT file and the value is the implementing function
"""

defmacro __using__(opts) do
macro_imports = Keyword.get(opts, :imports, %{})

genserver_setup =
quote do
use GenServer

def start_link(opts) do
Wasmex.Components.start_link(opts |> Keyword.put(:imports, unquote(macro_imports)))
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
Loading

0 comments on commit 460066f

Please sign in to comment.