From 757abe39b63e21169a3024d96d1a6390ae7e5b3a Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 3 Apr 2024 11:26:48 +1100 Subject: [PATCH 01/13] Allow reading globals --- lib/wasmex/instance.ex | 16 ++++++++++ lib/wasmex/native.ex | 3 ++ native/wasmex/src/instance.rs | 45 +++++++++++++++++++++++++++ native/wasmex/src/lib.rs | 1 + test/example_wasm_files/globals.wasm | Bin 0 -> 50 bytes test/example_wasm_files/globals.wat | 4 +++ test/wasmex/instance_test.exs | 16 ++++++++++ 7 files changed, 85 insertions(+) create mode 100644 test/example_wasm_files/globals.wasm create mode 100644 test/example_wasm_files/globals.wat diff --git a/lib/wasmex/instance.ex b/lib/wasmex/instance.ex index f07f8bb1..3627f390 100644 --- a/lib/wasmex/instance.ex +++ b/lib/wasmex/instance.ex @@ -172,6 +172,22 @@ defmodule Wasmex.Instance do def memory(store, instance) do Wasmex.Memory.from_instance(store, instance) end + + @doc ~S""" + Reads a global. + """ + @spec read_global(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary()) :: + {:ok, number()} | {:error, binary()} + def read_global(store_or_caller, instance, global_name) do + %{resource: store_or_caller_resource} = store_or_caller + %__MODULE__{resource: instance_resource} = instance + + Wasmex.Native.instance_read_global( + store_or_caller_resource, + instance_resource, + global_name + ) + end end defimpl Inspect, for: Wasmex.Instance do diff --git a/lib/wasmex/native.ex b/lib/wasmex/native.ex index eb7eed22..eb76ea31 100644 --- a/lib/wasmex/native.ex +++ b/lib/wasmex/native.ex @@ -52,6 +52,9 @@ defmodule Wasmex.Native do ), do: error() + def instance_read_global(_store_or_caller_resource, _instance_resource, _global_name), + do: error() + def memory_from_instance(_store_resource, _memory_resource), do: error() def memory_size(_store_resource, _memory_resource), do: error() def memory_grow(_store_resource, _memory_resource, _pages), do: error() diff --git a/native/wasmex/src/instance.rs b/native/wasmex/src/instance.rs index 14d1e173..4b8cc0f0 100644 --- a/native/wasmex/src/instance.rs +++ b/native/wasmex/src/instance.rs @@ -74,6 +74,51 @@ fn link_and_create_instance( .map_err(|err| Error::Term(Box::new(err.to_string()))) } +#[rustler::nif(name = "instance_read_global", schedule = "DirtyCpu")] +pub fn read_global( + env: rustler::Env, + store_or_caller_resource: ResourceArc, + instance_resource: ResourceArc, + global_name: String, +) -> NifResult { + let instance: Instance = *(instance_resource.inner.lock().map_err(|e| { + rustler::Error::Term(Box::new(format!( + "Could not unlock instance resource as the mutex was poisoned: {e}" + ))) + })?); + let mut store_or_caller: &mut StoreOrCaller = + &mut *(store_or_caller_resource.inner.lock().map_err(|e| { + rustler::Error::Term(Box::new(format!( + "Could not unlock instance/store resource as the mutex was poisoned: {e}" + ))) + })?); + + let global_val = instance + .get_global(&mut store_or_caller, &global_name) + .map(|g| g.get(&mut store_or_caller)); + + let value = global_val.ok_or_else(|| { + rustler::Error::Term(Box::new(format!( + "exported global `{global_name}` not found" + ))) + })?; + + match value { + Val::I32(i) => Ok(i.encode(env)), + Val::I64(i) => Ok(i.encode(env)), + Val::F32(i) => Ok(f32::from_bits(i).encode(env)), + Val::F64(i) => Ok(f64::from_bits(i).encode(env)), + // encoding V128 is not yet supported by rustler + Val::V128(_) => Err(rustler::Error::Term(Box::new("unable_to_return_v128_type"))), + Val::FuncRef(_) => Err(rustler::Error::Term(Box::new( + "unable_to_return_func_ref_type", + ))), + Val::ExternRef(_) => Err(rustler::Error::Term(Box::new( + "unable_to_return_extern_ref_type", + ))), + } +} + #[rustler::nif(name = "instance_function_export_exists")] pub fn function_export_exists( store_or_caller_resource: ResourceArc, diff --git a/native/wasmex/src/lib.rs b/native/wasmex/src/lib.rs index a8cc719b..9bc78443 100644 --- a/native/wasmex/src/lib.rs +++ b/native/wasmex/src/lib.rs @@ -20,6 +20,7 @@ rustler::init! { [ engine::new, engine::precompile_module, + instance::read_global, instance::call_exported_function, instance::function_export_exists, instance::new, diff --git a/test/example_wasm_files/globals.wasm b/test/example_wasm_files/globals.wasm new file mode 100644 index 0000000000000000000000000000000000000000..6898d27bde8de909566b60c5da5fa5de2557b6c2 GIT binary patch literal 50 zcmZQbEY4+QU|?Y5W~yg!)Z(sVbYkFUmuBM6O-; Date: Wed, 3 Apr 2024 12:06:56 +1100 Subject: [PATCH 02/13] Add setting of global values --- lib/wasmex/instance.ex | 23 +++++++- lib/wasmex/native.ex | 8 +++ native/wasmex/src/instance.rs | 76 +++++++++++++++++++++++++++ native/wasmex/src/lib.rs | 1 + test/example_wasm_files/globals.wasm | Bin 50 -> 0 bytes test/wasmex/instance_test.exs | 27 ++++++++-- 6 files changed, 130 insertions(+), 5 deletions(-) delete mode 100644 test/example_wasm_files/globals.wasm diff --git a/lib/wasmex/instance.ex b/lib/wasmex/instance.ex index 3627f390..095ce082 100644 --- a/lib/wasmex/instance.ex +++ b/lib/wasmex/instance.ex @@ -174,7 +174,7 @@ defmodule Wasmex.Instance do end @doc ~S""" - Reads a global. + Reads the value of an exported global. """ @spec read_global(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary()) :: {:ok, number()} | {:error, binary()} @@ -188,6 +188,27 @@ defmodule Wasmex.Instance do global_name ) end + + @doc ~S""" + Sets the value of an exported mutable global. + """ + @spec write_global(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary(), number()) :: + {:ok, number()} | {:error, binary()} + def write_global(store_or_caller, instance, global_name, new_value) do + %{resource: store_or_caller_resource} = store_or_caller + %__MODULE__{resource: instance_resource} = instance + + Wasmex.Native.instance_write_global( + store_or_caller_resource, + instance_resource, + global_name, + new_value + ) + |> case do + {} -> :ok + {:error, _reason} = term -> term + end + end end defimpl Inspect, for: Wasmex.Instance do diff --git a/lib/wasmex/native.ex b/lib/wasmex/native.ex index eb76ea31..cdef3f93 100644 --- a/lib/wasmex/native.ex +++ b/lib/wasmex/native.ex @@ -55,6 +55,14 @@ defmodule Wasmex.Native do def instance_read_global(_store_or_caller_resource, _instance_resource, _global_name), do: error() + def instance_write_global( + _store_or_caller_resource, + _instance_resource, + _global_name, + _new_value + ), + do: error() + def memory_from_instance(_store_resource, _memory_resource), do: error() def memory_size(_store_resource, _memory_resource), do: error() def memory_grow(_store_resource, _memory_resource, _pages), do: error() diff --git a/native/wasmex/src/instance.rs b/native/wasmex/src/instance.rs index 4b8cc0f0..88d373e4 100644 --- a/native/wasmex/src/instance.rs +++ b/native/wasmex/src/instance.rs @@ -119,6 +119,52 @@ pub fn read_global( } } +#[rustler::nif(name = "instance_write_global", schedule = "DirtyCpu")] +pub fn write_global( + env: rustler::Env, + store_or_caller_resource: ResourceArc, + instance_resource: ResourceArc, + global_name: String, + new_value: Term, +) -> NifResult<()> { + let instance: Instance = *(instance_resource.inner.lock().map_err(|e| { + rustler::Error::Term(Box::new(format!( + "Could not unlock instance resource as the mutex was poisoned: {e}" + ))) + })?); + let mut store_or_caller: &mut StoreOrCaller = + &mut *(store_or_caller_resource.inner.lock().map_err(|e| { + rustler::Error::Term(Box::new(format!( + "Could not unlock instance/store resource as the mutex was poisoned: {e}" + ))) + })?); + + let global = instance + .get_global(&mut store_or_caller, &global_name) + .ok_or_else(|| { + rustler::Error::Term(Box::new(format!( + "exported global `{global_name}` not found" + ))) + })?; + + let global_type = global.ty(&store_or_caller).content().clone(); + + let new_value = decode_term_as_wasm_value(global_type, new_value).ok_or_else(|| { + rustler::Error::Term(Box::new(format!("Cannot convert to a WebAssembly value."))) + })?; + + let val: Val = match new_value { + WasmValue::I32(value) => value.into(), + WasmValue::I64(value) => value.into(), + WasmValue::F32(value) => value.into(), + WasmValue::F64(value) => value.into(), + }; + + global + .set(&mut store_or_caller, val) + .map_err(|e| rustler::Error::Term(Box::new(format!("Could not set global: {e}")))) +} + #[rustler::nif(name = "instance_function_export_exists")] pub fn function_export_exists( store_or_caller_resource: ResourceArc, @@ -270,6 +316,36 @@ pub enum WasmValue { F64(f64), } +fn decode_term_as_wasm_value(expected_type: ValType, term: Term) -> Option { + let value = match (expected_type, term.get_type()) { + (ValType::I32, TermType::Integer | TermType::Float) => match term.decode::() { + Ok(value) => WasmValue::I32(value), + Err(_) => return None, + }, + (ValType::I64, TermType::Integer | TermType::Float) => match term.decode::() { + Ok(value) => WasmValue::I64(value), + Err(_) => return None, + }, + (ValType::F32, TermType::Integer | TermType::Float) => match term.decode::() { + Ok(value) => { + if value.is_finite() { + WasmValue::F32(value) + } else { + return None; + } + } + Err(_) => return None, + }, + (ValType::F64, TermType::Integer | TermType::Float) => match term.decode::() { + Ok(value) => WasmValue::F64(value), + Err(_) => return None, + }, + (_val_type, _term_type) => return None, + }; + + Some(value) +} + pub fn decode_function_param_terms( params: &[ValType], function_param_terms: Vec, diff --git a/native/wasmex/src/lib.rs b/native/wasmex/src/lib.rs index 9bc78443..8f343a87 100644 --- a/native/wasmex/src/lib.rs +++ b/native/wasmex/src/lib.rs @@ -21,6 +21,7 @@ rustler::init! { engine::new, engine::precompile_module, instance::read_global, + instance::write_global, instance::call_exported_function, instance::function_export_exists, instance::new, diff --git a/test/example_wasm_files/globals.wasm b/test/example_wasm_files/globals.wasm deleted file mode 100644 index 6898d27bde8de909566b60c5da5fa5de2557b6c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50 zcmZQbEY4+QU|?Y5W~yg!)Z(sVbYkFUmuBM6O-; Date: Wed, 3 Apr 2024 12:12:14 +1100 Subject: [PATCH 03/13] Rename to {get,set}_global_value --- lib/wasmex/instance.ex | 12 ++++++------ lib/wasmex/native.ex | 4 ++-- native/wasmex/src/instance.rs | 8 ++++---- native/wasmex/src/lib.rs | 4 ++-- test/wasmex/instance_test.exs | 18 +++++++++--------- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/wasmex/instance.ex b/lib/wasmex/instance.ex index 095ce082..26a24a69 100644 --- a/lib/wasmex/instance.ex +++ b/lib/wasmex/instance.ex @@ -176,13 +176,13 @@ defmodule Wasmex.Instance do @doc ~S""" Reads the value of an exported global. """ - @spec read_global(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary()) :: + @spec get_global_value(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary()) :: {:ok, number()} | {:error, binary()} - def read_global(store_or_caller, instance, global_name) do + def get_global_value(store_or_caller, instance, global_name) do %{resource: store_or_caller_resource} = store_or_caller %__MODULE__{resource: instance_resource} = instance - Wasmex.Native.instance_read_global( + Wasmex.Native.instance_get_global_value( store_or_caller_resource, instance_resource, global_name @@ -192,13 +192,13 @@ defmodule Wasmex.Instance do @doc ~S""" Sets the value of an exported mutable global. """ - @spec write_global(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary(), number()) :: + @spec set_global_value(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary(), number()) :: {:ok, number()} | {:error, binary()} - def write_global(store_or_caller, instance, global_name, new_value) do + def set_global_value(store_or_caller, instance, global_name, new_value) do %{resource: store_or_caller_resource} = store_or_caller %__MODULE__{resource: instance_resource} = instance - Wasmex.Native.instance_write_global( + Wasmex.Native.instance_set_global_value( store_or_caller_resource, instance_resource, global_name, diff --git a/lib/wasmex/native.ex b/lib/wasmex/native.ex index cdef3f93..6426be89 100644 --- a/lib/wasmex/native.ex +++ b/lib/wasmex/native.ex @@ -52,10 +52,10 @@ defmodule Wasmex.Native do ), do: error() - def instance_read_global(_store_or_caller_resource, _instance_resource, _global_name), + def instance_get_global_value(_store_or_caller_resource, _instance_resource, _global_name), do: error() - def instance_write_global( + def instance_set_global_value( _store_or_caller_resource, _instance_resource, _global_name, diff --git a/native/wasmex/src/instance.rs b/native/wasmex/src/instance.rs index 88d373e4..992c3d29 100644 --- a/native/wasmex/src/instance.rs +++ b/native/wasmex/src/instance.rs @@ -74,8 +74,8 @@ fn link_and_create_instance( .map_err(|err| Error::Term(Box::new(err.to_string()))) } -#[rustler::nif(name = "instance_read_global", schedule = "DirtyCpu")] -pub fn read_global( +#[rustler::nif(name = "instance_get_global_value", schedule = "DirtyCpu")] +pub fn get_global_value( env: rustler::Env, store_or_caller_resource: ResourceArc, instance_resource: ResourceArc, @@ -119,8 +119,8 @@ pub fn read_global( } } -#[rustler::nif(name = "instance_write_global", schedule = "DirtyCpu")] -pub fn write_global( +#[rustler::nif(name = "instance_set_global_value", schedule = "DirtyCpu")] +pub fn set_global_value( env: rustler::Env, store_or_caller_resource: ResourceArc, instance_resource: ResourceArc, diff --git a/native/wasmex/src/lib.rs b/native/wasmex/src/lib.rs index 8f343a87..d9a5da1a 100644 --- a/native/wasmex/src/lib.rs +++ b/native/wasmex/src/lib.rs @@ -20,8 +20,8 @@ rustler::init! { [ engine::new, engine::precompile_module, - instance::read_global, - instance::write_global, + instance::get_global_value, + instance::set_global_value, instance::call_exported_function, instance::function_export_exists, instance::new, diff --git a/test/wasmex/instance_test.exs b/test/wasmex/instance_test.exs index 16e856e0..57a2856a 100644 --- a/test/wasmex/instance_test.exs +++ b/test/wasmex/instance_test.exs @@ -205,29 +205,29 @@ defmodule Wasmex.InstanceTest do %{instance: instance, store: store} end - test "getting a global value", context do + test t(&Wasmex.Instance.get_global_value/3), context do store = context[:store] instance = context[:instance] assert {:error, "exported global `unknown_global` not found"} = - Wasmex.Instance.read_global(store, instance, "unknown_global") + Wasmex.Instance.get_global_value(store, instance, "unknown_global") - assert 42 = Wasmex.Instance.read_global(store, instance, "meaning_of_life") - assert 0 = Wasmex.Instance.read_global(store, instance, "count") + assert 42 = Wasmex.Instance.get_global_value(store, instance, "meaning_of_life") + assert 0 = Wasmex.Instance.get_global_value(store, instance, "count") end - test "setting a global value", context do + test t(&Wasmex.Instance.set_global_value/4), context do store = context[:store] instance = context[:instance] assert {:error, "exported global `unknown_global` not found"} = - Wasmex.Instance.write_global(store, instance, "unknown_global", 0) + Wasmex.Instance.set_global_value(store, instance, "unknown_global", 0) assert {:error, "Could not set global: immutable global cannot be set"} = - Wasmex.Instance.write_global(store, instance, "meaning_of_life", 0) + Wasmex.Instance.set_global_value(store, instance, "meaning_of_life", 0) - assert :ok = Wasmex.Instance.write_global(store, instance, "count", 99) - assert 99 = Wasmex.Instance.read_global(store, instance, "count") + assert :ok = Wasmex.Instance.set_global_value(store, instance, "count", 99) + assert 99 = Wasmex.Instance.get_global_value(store, instance, "count") end end end From 3923979de37fdfa7a57d1938c3b91cbc177223e1 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 3 Apr 2024 12:16:58 +1100 Subject: [PATCH 04/13] Test more global types --- test/example_wasm_files/globals.wat | 5 ++++- test/wasmex/instance_test.exs | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/test/example_wasm_files/globals.wat b/test/example_wasm_files/globals.wat index fde13d38..94c191b1 100644 --- a/test/example_wasm_files/globals.wat +++ b/test/example_wasm_files/globals.wat @@ -1,4 +1,7 @@ (module (global $meaning_of_life (export "meaning_of_life") i32 (i32.const 42)) - (global $count (export "count") (mut i64) (i64.const 0)) + (global (export "count_32") (mut i32) (i32.const -32)) + (global (export "count_64") (mut i64) (i64.const -64)) + (global (export "bad_pi_32") (mut f32) (f32.const 0)) + (global (export "bad_pi_64") (mut f64) (f64.const 0)) ) \ No newline at end of file diff --git a/test/wasmex/instance_test.exs b/test/wasmex/instance_test.exs index 57a2856a..3d7dffee 100644 --- a/test/wasmex/instance_test.exs +++ b/test/wasmex/instance_test.exs @@ -213,7 +213,8 @@ defmodule Wasmex.InstanceTest do Wasmex.Instance.get_global_value(store, instance, "unknown_global") assert 42 = Wasmex.Instance.get_global_value(store, instance, "meaning_of_life") - assert 0 = Wasmex.Instance.get_global_value(store, instance, "count") + assert -32 = Wasmex.Instance.get_global_value(store, instance, "count_32") + assert -64 = Wasmex.Instance.get_global_value(store, instance, "count_64") end test t(&Wasmex.Instance.set_global_value/4), context do @@ -226,8 +227,17 @@ defmodule Wasmex.InstanceTest do assert {:error, "Could not set global: immutable global cannot be set"} = Wasmex.Instance.set_global_value(store, instance, "meaning_of_life", 0) - assert :ok = Wasmex.Instance.set_global_value(store, instance, "count", 99) - assert 99 = Wasmex.Instance.get_global_value(store, instance, "count") + assert :ok = Wasmex.Instance.set_global_value(store, instance, "count_32", 99) + assert 99 = Wasmex.Instance.get_global_value(store, instance, "count_32") + + assert :ok = Wasmex.Instance.set_global_value(store, instance, "count_64", 17) + assert 17 = Wasmex.Instance.get_global_value(store, instance, "count_64") + + assert :ok = Wasmex.Instance.set_global_value(store, instance, "bad_pi_32", 3.14) + assert_in_delta 3.14, Wasmex.Instance.get_global_value(store, instance, "bad_pi_32"), 0.01 + + assert :ok = Wasmex.Instance.set_global_value(store, instance, "bad_pi_64", 3.14) + assert_in_delta 3.14, Wasmex.Instance.get_global_value(store, instance, "bad_pi_64"), 0.01 end end end From fbc8b06f331196ca5fc45108ca2938fa1f484bb9 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 3 Apr 2024 12:21:19 +1100 Subject: [PATCH 05/13] Change get_global_value to return {:ok, result} --- lib/wasmex/instance.ex | 4 ++++ test/wasmex/instance_test.exs | 20 +++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/wasmex/instance.ex b/lib/wasmex/instance.ex index 26a24a69..a395f727 100644 --- a/lib/wasmex/instance.ex +++ b/lib/wasmex/instance.ex @@ -187,6 +187,10 @@ defmodule Wasmex.Instance do instance_resource, global_name ) + |> case do + {:error, _reason} = term -> term + result when is_number(result) -> {:ok, result} + end end @doc ~S""" diff --git a/test/wasmex/instance_test.exs b/test/wasmex/instance_test.exs index 3d7dffee..1f01160f 100644 --- a/test/wasmex/instance_test.exs +++ b/test/wasmex/instance_test.exs @@ -212,9 +212,9 @@ defmodule Wasmex.InstanceTest do assert {:error, "exported global `unknown_global` not found"} = Wasmex.Instance.get_global_value(store, instance, "unknown_global") - assert 42 = Wasmex.Instance.get_global_value(store, instance, "meaning_of_life") - assert -32 = Wasmex.Instance.get_global_value(store, instance, "count_32") - assert -64 = Wasmex.Instance.get_global_value(store, instance, "count_64") + assert {:ok, 42} = Wasmex.Instance.get_global_value(store, instance, "meaning_of_life") + assert {:ok, -32} = Wasmex.Instance.get_global_value(store, instance, "count_32") + assert {:ok, -64} = Wasmex.Instance.get_global_value(store, instance, "count_64") end test t(&Wasmex.Instance.set_global_value/4), context do @@ -228,16 +228,22 @@ defmodule Wasmex.InstanceTest do Wasmex.Instance.set_global_value(store, instance, "meaning_of_life", 0) assert :ok = Wasmex.Instance.set_global_value(store, instance, "count_32", 99) - assert 99 = Wasmex.Instance.get_global_value(store, instance, "count_32") + assert {:ok, 99} = Wasmex.Instance.get_global_value(store, instance, "count_32") assert :ok = Wasmex.Instance.set_global_value(store, instance, "count_64", 17) - assert 17 = Wasmex.Instance.get_global_value(store, instance, "count_64") + assert {:ok, 17} = Wasmex.Instance.get_global_value(store, instance, "count_64") assert :ok = Wasmex.Instance.set_global_value(store, instance, "bad_pi_32", 3.14) - assert_in_delta 3.14, Wasmex.Instance.get_global_value(store, instance, "bad_pi_32"), 0.01 + + assert_in_delta 3.14, + elem(Wasmex.Instance.get_global_value(store, instance, "bad_pi_32"), 1), + 0.01 assert :ok = Wasmex.Instance.set_global_value(store, instance, "bad_pi_64", 3.14) - assert_in_delta 3.14, Wasmex.Instance.get_global_value(store, instance, "bad_pi_64"), 0.01 + + assert_in_delta 3.14, + elem(Wasmex.Instance.get_global_value(store, instance, "bad_pi_64"), 1), + 0.01 end end end From ebc8ec1dc13314ff57d18dcbb2104679f056fa2a Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 3 Apr 2024 14:30:30 +1100 Subject: [PATCH 06/13] Fix linting issues --- native/wasmex/src/instance.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/native/wasmex/src/instance.rs b/native/wasmex/src/instance.rs index 992c3d29..97e84462 100644 --- a/native/wasmex/src/instance.rs +++ b/native/wasmex/src/instance.rs @@ -121,7 +121,6 @@ pub fn get_global_value( #[rustler::nif(name = "instance_set_global_value", schedule = "DirtyCpu")] pub fn set_global_value( - env: rustler::Env, store_or_caller_resource: ResourceArc, instance_resource: ResourceArc, global_name: String, @@ -149,9 +148,8 @@ pub fn set_global_value( let global_type = global.ty(&store_or_caller).content().clone(); - let new_value = decode_term_as_wasm_value(global_type, new_value).ok_or_else(|| { - rustler::Error::Term(Box::new(format!("Cannot convert to a WebAssembly value."))) - })?; + let new_value = decode_term_as_wasm_value(global_type, new_value) + .ok_or_else(|| rustler::Error::Term(Box::new("Cannot convert to a WebAssembly value.")))?; let val: Val = match new_value { WasmValue::I32(value) => value.into(), From f651ad433706a042eb6ad19c323d6117c9c2a85d Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 3 Apr 2024 14:50:26 +1100 Subject: [PATCH 07/13] DRY --- native/wasmex/src/instance.rs | 68 +++++++---------------------------- 1 file changed, 13 insertions(+), 55 deletions(-) diff --git a/native/wasmex/src/instance.rs b/native/wasmex/src/instance.rs index 97e84462..69393559 100644 --- a/native/wasmex/src/instance.rs +++ b/native/wasmex/src/instance.rs @@ -362,65 +362,23 @@ pub fn decode_function_param_terms( .zip(function_param_terms.into_iter()) .enumerate() { - let value = match (param, given_param.get_type()) { - (ValType::I32, TermType::Integer | TermType::Float) => { - match given_param.decode::() { - Ok(value) => WasmValue::I32(value), - Err(_) => { - return Err(format!( - "Cannot convert argument #{} to a WebAssembly i32 value.", - nth + 1 - )); - } - } - } - (ValType::I64, TermType::Integer | TermType::Float) => { - match given_param.decode::() { - Ok(value) => WasmValue::I64(value), - Err(_) => { - return Err(format!( - "Cannot convert argument #{} to a WebAssembly i64 value.", - nth + 1 - )); - } - } - } - (ValType::F32, TermType::Integer | TermType::Float) => { - match given_param.decode::() { - Ok(value) => { - if value.is_finite() { - WasmValue::F32(value) - } else { - return Err(format!( - "Cannot convert argument #{} to a WebAssembly f32 value.", - nth + 1 - )); - } - } - Err(_) => { - return Err(format!( - "Cannot convert argument #{} to a WebAssembly f32 value.", - nth + 1 - )); - } - } - } - (ValType::F64, TermType::Integer | TermType::Float) => { - match given_param.decode::() { - Ok(value) => WasmValue::F64(value), - Err(_) => { - return Err(format!( - "Cannot convert argument #{} to a WebAssembly f64 value.", - nth + 1 - )); - } - } + let value = match ( + decode_term_as_wasm_value(param.clone(), given_param), + given_param.get_type(), + ) { + (Some(value), _) => value, + (_, TermType::Integer | TermType::Float) => { + return Err(format!( + "Cannot convert argument #{} to a WebAssembly {} value.", + nth + 1, + format!("{:?}", param).to_lowercase() + )) } - (val_type, term_type) => { + (_, term_type) => { return Err(format!( "Cannot convert argument #{} to a WebAssembly {:?} value. Given `{:?}`.", nth + 1, - val_type, + param, PrintableTermType::PrintTerm(term_type) )); } From 42f4e3dae3fe7301367dc5198653943f46bbefa5 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 3 Apr 2024 14:54:10 +1100 Subject: [PATCH 08/13] More informative error message --- native/wasmex/src/instance.rs | 9 +++++++-- test/wasmex/instance_test.exs | 3 +++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/native/wasmex/src/instance.rs b/native/wasmex/src/instance.rs index 69393559..f8ccb0b5 100644 --- a/native/wasmex/src/instance.rs +++ b/native/wasmex/src/instance.rs @@ -148,8 +148,13 @@ pub fn set_global_value( let global_type = global.ty(&store_or_caller).content().clone(); - let new_value = decode_term_as_wasm_value(global_type, new_value) - .ok_or_else(|| rustler::Error::Term(Box::new("Cannot convert to a WebAssembly value.")))?; + let new_value = decode_term_as_wasm_value(global_type.clone(), new_value).ok_or_else(|| { + rustler::Error::Term(Box::new(format!( + "Cannot convert to a WebAssembly {:?} value. Given `{:?}`.", + global_type, + PrintableTermType::PrintTerm(new_value.get_type()) + ))) + })?; let val: Val = match new_value { WasmValue::I32(value) => value.into(), diff --git a/test/wasmex/instance_test.exs b/test/wasmex/instance_test.exs index 1f01160f..e33606e5 100644 --- a/test/wasmex/instance_test.exs +++ b/test/wasmex/instance_test.exs @@ -227,6 +227,9 @@ defmodule Wasmex.InstanceTest do assert {:error, "Could not set global: immutable global cannot be set"} = Wasmex.Instance.set_global_value(store, instance, "meaning_of_life", 0) + assert {:error, "Cannot convert to a WebAssembly I32 value. Given `Atom`."} = + Wasmex.Instance.set_global_value(store, instance, "count_32", :abc) + assert :ok = Wasmex.Instance.set_global_value(store, instance, "count_32", 99) assert {:ok, 99} = Wasmex.Instance.get_global_value(store, instance, "count_32") From 85004c72aec7369a641d81cf8bd2ab81f85b6447 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 3 Apr 2024 14:59:48 +1100 Subject: [PATCH 09/13] Minor refactor to return error earlier --- native/wasmex/src/instance.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/native/wasmex/src/instance.rs b/native/wasmex/src/instance.rs index f8ccb0b5..ba12f341 100644 --- a/native/wasmex/src/instance.rs +++ b/native/wasmex/src/instance.rs @@ -93,15 +93,15 @@ pub fn get_global_value( ))) })?); - let global_val = instance + let global = instance .get_global(&mut store_or_caller, &global_name) - .map(|g| g.get(&mut store_or_caller)); + .ok_or_else(|| { + rustler::Error::Term(Box::new(format!( + "exported global `{global_name}` not found" + ))) + })?; - let value = global_val.ok_or_else(|| { - rustler::Error::Term(Box::new(format!( - "exported global `{global_name}` not found" - ))) - })?; + let value = global.get(&mut store_or_caller); match value { Val::I32(i) => Ok(i.encode(env)), From 3eac8b87f4cb17e47ac35a8315123cdcb812a447 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 3 Apr 2024 15:16:59 +1100 Subject: [PATCH 10/13] Add doctests --- lib/wasmex/instance.ex | 28 ++++++++++++++++++++++++++++ test/wasmex/instance_test.exs | 4 ++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/wasmex/instance.ex b/lib/wasmex/instance.ex index a395f727..bf90b12e 100644 --- a/lib/wasmex/instance.ex +++ b/lib/wasmex/instance.ex @@ -175,6 +175,20 @@ defmodule Wasmex.Instance do @doc ~S""" Reads the value of an exported global. + + ## Examples + + iex> wat = "(module + ...> (global $answer i32 (i32.const 42)) + ...> (export \"answer\" (global $answer)) + ...> )" + iex> {:ok, store} = Wasmex.Store.new() + iex> {:ok, module} = Wasmex.Module.compile(store, wat) + iex> {:ok, instance} = Wasmex.Instance.new(store, module, %{}) + iex> Wasmex.Instance.get_global_value(store, instance, "answer") + {:ok, 42} + iex> Wasmex.Instance.get_global_value(store, instance, "not_a_global") + {:error, "exported global `not_a_global` not found"} """ @spec get_global_value(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary()) :: {:ok, number()} | {:error, binary()} @@ -195,6 +209,20 @@ defmodule Wasmex.Instance do @doc ~S""" Sets the value of an exported mutable global. + + ## Examples + + iex> wat = "(module + ...> (global $count (mut i32) (i32.const 0)) + ...> (export \"count\" (global $count)) + ...> )" + iex> {:ok, store} = Wasmex.Store.new() + iex> {:ok, module} = Wasmex.Module.compile(store, wat) + iex> {:ok, instance} = Wasmex.Instance.new(store, module, %{}) + iex> Wasmex.Instance.set_global_value(store, instance, "count", 1) + :ok + iex> Wasmex.Instance.get_global_value(store, instance, "count") + {:ok, 1} """ @spec set_global_value(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary(), number()) :: {:ok, number()} | {:error, binary()} diff --git a/test/wasmex/instance_test.exs b/test/wasmex/instance_test.exs index e33606e5..068007e0 100644 --- a/test/wasmex/instance_test.exs +++ b/test/wasmex/instance_test.exs @@ -198,9 +198,9 @@ defmodule Wasmex.InstanceTest do describe "globals" do setup do - source = File.read!("#{Path.dirname(__ENV__.file)}/../example_wasm_files/globals.wat") + wat = File.read!("#{Path.dirname(__ENV__.file)}/../example_wasm_files/globals.wat") {:ok, store} = Wasmex.Store.new() - {:ok, module} = Wasmex.Module.compile(store, source) + {:ok, module} = Wasmex.Module.compile(store, wat) {:ok, instance} = Wasmex.Instance.new(store, module, %{}) %{instance: instance, store: store} end From b1cf1908ff61fb7e1900c0e3a2d9d1f6f7dca7a0 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 5 Apr 2024 16:55:06 +1100 Subject: [PATCH 11/13] Test global ref types err --- test/example_wasm_files/globals.wat | 2 ++ test/wasmex/instance_test.exs | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/test/example_wasm_files/globals.wat b/test/example_wasm_files/globals.wat index 94c191b1..efefaade 100644 --- a/test/example_wasm_files/globals.wat +++ b/test/example_wasm_files/globals.wat @@ -2,6 +2,8 @@ (global $meaning_of_life (export "meaning_of_life") i32 (i32.const 42)) (global (export "count_32") (mut i32) (i32.const -32)) (global (export "count_64") (mut i64) (i64.const -64)) + (global (export "externref") externref (ref.null extern)) + (global (export "funcref") funcref (ref.null func)) (global (export "bad_pi_32") (mut f32) (f32.const 0)) (global (export "bad_pi_64") (mut f64) (f64.const 0)) ) \ No newline at end of file diff --git a/test/wasmex/instance_test.exs b/test/wasmex/instance_test.exs index 068007e0..e74d5e1f 100644 --- a/test/wasmex/instance_test.exs +++ b/test/wasmex/instance_test.exs @@ -215,6 +215,12 @@ defmodule Wasmex.InstanceTest do assert {:ok, 42} = Wasmex.Instance.get_global_value(store, instance, "meaning_of_life") assert {:ok, -32} = Wasmex.Instance.get_global_value(store, instance, "count_32") assert {:ok, -64} = Wasmex.Instance.get_global_value(store, instance, "count_64") + + assert {:error, "unable_to_return_extern_ref_type"} = + Wasmex.Instance.get_global_value(store, instance, "externref") + + assert {:error, "unable_to_return_func_ref_type"} = + Wasmex.Instance.get_global_value(store, instance, "funcref") end test t(&Wasmex.Instance.set_global_value/4), context do From a7c6a4c89b2c26f1f6a5f1318571b0d82d3ff38c Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 5 Apr 2024 17:17:08 +1100 Subject: [PATCH 12/13] Update changelog --- CHANGELOG.md | 89 ++++++++++++++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7178f1b..e95c52c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,46 +21,47 @@ Wasmtime rewrote their fuel-related API and simplified it. To remain consistent The underlying implementation of the fuel system got rewritten as well. If you are using fuel in your app, please check your fuel consumption values. -* Thanks to @RoyalIcing for helping us keeping our dependencies up to date for this release 💜 +- Thanks to @RoyalIcing for helping us keeping our dependencies up to date for this release 💜 ### Added -* official support for Elixir 1.15 and 1.16 -* fuel-related API got rewritten, because the underlying Wasm library (wasmtime) changed their API and we want to be consistent. Added `Store.get_fuel/1` and `Store.set_fuel/2` which is a much simpler API than before. +- official support for Elixir 1.15 and 1.16 +- fuel-related API got rewritten, because the underlying Wasm library (wasmtime) changed their API and we want to be consistent. Added `Store.get_fuel/1` and `Store.set_fuel/2` which is a much simpler API than before. +- read and write a global’s value with `Instance.get_global_value/3` and `Instance.set_global_value/4` (#540) ### Removed -* removed support for Elixir 1.12 -* with the fuel-related API changed, the existing methods on `Store` (`consume_fuel`, `fuel_remaining`, `add_fuel`) were removed. Please call `set_fuel/2` and `get_fuel/1` instead. +- removed support for Elixir 1.12 +- with the fuel-related API changed, the existing methods on `Store` (`consume_fuel`, `fuel_remaining`, `add_fuel`) were removed. Please call `set_fuel/2` and `get_fuel/1` instead. ### Changed -* Dependency updates (most notably wasmtime and rustler) -* removed dialyzer +- Dependency updates (most notably wasmtime and rustler) +- removed dialyzer ## [0.8.4] - 2023-06-?? ### Added -* added support for multi-value returns from WASM and elixir callbacks. This enables passing string return values safely by pointer and length, for example. +- added support for multi-value returns from WASM and elixir callbacks. This enables passing string return values safely by pointer and length, for example. ## [0.8.3] - 2023-05-24 ### Added -* added support for `riscv64gc-unknown-linux-gnu` -* added support for OTP 26 +- added support for `riscv64gc-unknown-linux-gnu` +- added support for OTP 26 ### Changed -* updated rustler from 0.27.0 to 0.28.0 -* updated wasmtime from 4.0.1 to 9.0.1 +- updated rustler from 0.27.0 to 0.28.0 +- updated wasmtime from 4.0.1 to 9.0.1 ## [0.8.2] - 2023-01-08 ## Added -* list `aarch64-unknown-linux-musl` in rustler targets, so we actually include it in our releases +- list `aarch64-unknown-linux-musl` in rustler targets, so we actually include it in our releases ## [0.8.1] - 2023-01-08 @@ -75,22 +76,21 @@ Today, a `Wasmex.Engine` already gives us a faster way to precompile modules wit ### Added -* Added precompiled binary for `aarch64-unknown-linux-musl` -* Added support for setting store limits. This allows users to limit memory usage, instance creation, table sizes and more. See `Wasmex.StoreLimits` for details. -* Added support for metering/fuel_consumption. This allows users to limit CPU usage. A `Wasmex.Store` can be given fuel, where each Wasm instruction of a running Wasm binary uses a certain amount of fuel. If no fuel remains, execution stops. See `Wasmex.EngineConfig` for details. -* Added `Wasmex.EngineConfig` as a place for more complex Wasm settings. With this release an engine can be configured to provide more detailed backtraces on errors during Wasm execution by setting the `wasm_backtrace_details` flag. -* Added `Wasmex.Engine.precompile_module/2` which allows module precompilation from a .wat or .wasm binary without the need to instantiate said module. A precompiled module can be hydrated with `Module.unsafe_deserialize/2`. -* Added `Wasmex.module/1` and `Wasmex.store/1` to access the module and store of a running Wasmex GenServer process. -* Added option to `Wasmex.EngineConfig` to configure the `cranelift_opt_level` (:none, :speed, :speed_and_size) allowing users to trade compilation time against execution speed +- Added precompiled binary for `aarch64-unknown-linux-musl` +- Added support for setting store limits. This allows users to limit memory usage, instance creation, table sizes and more. See `Wasmex.StoreLimits` for details. +- Added support for metering/fuel_consumption. This allows users to limit CPU usage. A `Wasmex.Store` can be given fuel, where each Wasm instruction of a running Wasm binary uses a certain amount of fuel. If no fuel remains, execution stops. See `Wasmex.EngineConfig` for details. +- Added `Wasmex.EngineConfig` as a place for more complex Wasm settings. With this release an engine can be configured to provide more detailed backtraces on errors during Wasm execution by setting the `wasm_backtrace_details` flag. +- Added `Wasmex.Engine.precompile_module/2` which allows module precompilation from a .wat or .wasm binary without the need to instantiate said module. A precompiled module can be hydrated with `Module.unsafe_deserialize/2`. +- Added `Wasmex.module/1` and `Wasmex.store/1` to access the module and store of a running Wasmex GenServer process. +- Added option to `Wasmex.EngineConfig` to configure the `cranelift_opt_level` (:none, :speed, :speed_and_size) allowing users to trade compilation time against execution speed ### Changed -* `mix.exs` now also requires at least Elixir 1.12 -* `Module.unsafe_deserialize/2` now accepts a `Wasmex.Engine` in addition to the serialized module binary. It's best to hydrate a module using the same engine config used to serialize or precompile it. It has no harsh consequences today, but will be important when we add more Wasm features (e.g. SIMD support) in the future. -* added typespecs for all public `Wasmex` methods -* improved documentation and typespecs -* allow starting the `Wasmex` GenServer with a `%{bytes: bytes, store: store}` map as a convenience to spare users the task of manually compiling a `Wasmex.Module` - +- `mix.exs` now also requires at least Elixir 1.12 +- `Module.unsafe_deserialize/2` now accepts a `Wasmex.Engine` in addition to the serialized module binary. It's best to hydrate a module using the same engine config used to serialize or precompile it. It has no harsh consequences today, but will be important when we add more Wasm features (e.g. SIMD support) in the future. +- added typespecs for all public `Wasmex` methods +- improved documentation and typespecs +- allow starting the `Wasmex` GenServer with a `%{bytes: bytes, store: store}` map as a convenience to spare users the task of manually compiling a `Wasmex.Module` ## [0.8.0] - 2023-01-03 @@ -108,30 +108,30 @@ Please visit the list of changes below for more details. ### Added -* Added support for OTP 25 -* Added support for Elixir 1.14 +- Added support for OTP 25 +- Added support for Elixir 1.14 ### Removed -* Removed official support for OTP 22 and 23 -* Removed official support for Elixir 1.12 -* Removed `Wasmex.Module.set_name()` without replacement as this is not supported by Wasmtime -* Removed `Wasmex.Memory.bytes_per_element()` without replacement because we dropped support for different data types and now only handle bytes -* Removed `Wasmex.Pipe.set_len()` without replacement -* WASI directory/file preopens can not configure read/write/create permissions anymore because wasmtime does not support this feature well. We very much plan to add support back [once wasmtime allows](https://github.com/bytecodealliance/wasmtime/issues/4273). +- Removed official support for OTP 22 and 23 +- Removed official support for Elixir 1.12 +- Removed `Wasmex.Module.set_name()` without replacement as this is not supported by Wasmtime +- Removed `Wasmex.Memory.bytes_per_element()` without replacement because we dropped support for different data types and now only handle bytes +- Removed `Wasmex.Pipe.set_len()` without replacement +- WASI directory/file preopens can not configure read/write/create permissions anymore because wasmtime does not support this feature well. We very much plan to add support back [once wasmtime allows](https://github.com/bytecodealliance/wasmtime/issues/4273). ### Changed -* Changed the underlying Wasm engine from wasmer to [wasmtime](https://wasmtime.dev) -* Removed `Wasmex.Instance.new()` and `Wasmex.Instance.new_wasi()` in favor of `Wasmex.Store.new()` and `Wasmex.Store.new_wasi()`. -* WASI-options to `Wasmex.Store.new_wasi()` are now a proper struct `Wasmex.Wasi.WasiOptions` to improve typespecs, docs, and compile-time warnings. -* `Wasmex.Pipe` went through an internal rewrite. It is now a positioned read/write stream. You may change the read/write position with `Wasmex.Pipe.seek()` -* Renamed `Wasmex.Pipe.create()` to `Wasmex.Pipe.new()` to be consistent with other struct-creation calls -* Renamed `Wasmex.Memory.length()` to `Wasmex.Memory.size()` for consistenct with other `size` methods -* Renamed `Wasmex.Memory.set()` to `Wasmex.Memory.set_byte()` -* Renamed `Wasmex.Memory.get()` to `Wasmex.Memory.get_byte()` -* Updated and rewrote most of the docs - all examples are now doctests and tested on CI -* Updated all Elixir/Rust dependencies +- Changed the underlying Wasm engine from wasmer to [wasmtime](https://wasmtime.dev) +- Removed `Wasmex.Instance.new()` and `Wasmex.Instance.new_wasi()` in favor of `Wasmex.Store.new()` and `Wasmex.Store.new_wasi()`. +- WASI-options to `Wasmex.Store.new_wasi()` are now a proper struct `Wasmex.Wasi.WasiOptions` to improve typespecs, docs, and compile-time warnings. +- `Wasmex.Pipe` went through an internal rewrite. It is now a positioned read/write stream. You may change the read/write position with `Wasmex.Pipe.seek()` +- Renamed `Wasmex.Pipe.create()` to `Wasmex.Pipe.new()` to be consistent with other struct-creation calls +- Renamed `Wasmex.Memory.length()` to `Wasmex.Memory.size()` for consistenct with other `size` methods +- Renamed `Wasmex.Memory.set()` to `Wasmex.Memory.set_byte()` +- Renamed `Wasmex.Memory.get()` to `Wasmex.Memory.get_byte()` +- Updated and rewrote most of the docs - all examples are now doctests and tested on CI +- Updated all Elixir/Rust dependencies ## [0.7.1] - 2022-05-25 @@ -139,7 +139,6 @@ Please visit the list of changes below for more details. - Added an optional fourth parameter to `call_function`, `timeout`, which accepts a value in milliseconds that will cap the execution time of the function. The default behavior if not supplied is preserved, which is a 5 second timeout. Thanks @brooksmtownsend for this contribution - ## [0.7.0] - 2022-03-27 ### Added From 09f3edf4ded2122a58eeeb62ac7e21537d2ebdaf Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 5 Apr 2024 17:17:26 +1100 Subject: [PATCH 13/13] Link PR in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e95c52c4..340a1534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ please check your fuel consumption values. - official support for Elixir 1.15 and 1.16 - fuel-related API got rewritten, because the underlying Wasm library (wasmtime) changed their API and we want to be consistent. Added `Store.get_fuel/1` and `Store.set_fuel/2` which is a much simpler API than before. -- read and write a global’s value with `Instance.get_global_value/3` and `Instance.set_global_value/4` (#540) +- read and write a global’s value with `Instance.get_global_value/3` and `Instance.set_global_value/4` ([#540](https://github.com/tessi/wasmex/pull/540)) ### Removed