Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get and Set Globals #540

Merged
merged 13 commits into from
Apr 5, 2024
41 changes: 41 additions & 0 deletions lib/wasmex/instance.ex
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,47 @@ defmodule Wasmex.Instance do
def memory(store, instance) do
Wasmex.Memory.from_instance(store, instance)
end

@doc ~S"""
Reads the value of an exported global.
RoyalIcing marked this conversation as resolved.
Show resolved Hide resolved
"""
@spec get_global_value(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary()) ::
{:ok, number()} | {:error, binary()}
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_get_global_value(
store_or_caller_resource,
instance_resource,
global_name
)
|> case do
{:error, _reason} = term -> term
result when is_number(result) -> {:ok, result}
end
end

@doc ~S"""
Sets the value of an exported mutable global.
"""
@spec set_global_value(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary(), number()) ::
{:ok, number()} | {:error, binary()}
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_set_global_value(
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
Expand Down
11 changes: 11 additions & 0 deletions lib/wasmex/native.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ defmodule Wasmex.Native do
),
do: error()

def instance_get_global_value(_store_or_caller_resource, _instance_resource, _global_name),
do: error()

def instance_set_global_value(
_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()
Expand Down
121 changes: 121 additions & 0 deletions native/wasmex/src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,97 @@ fn link_and_create_instance(
.map_err(|err| Error::Term(Box::new(err.to_string())))
}

#[rustler::nif(name = "instance_get_global_value", schedule = "DirtyCpu")]
pub fn get_global_value(
env: rustler::Env,
store_or_caller_resource: ResourceArc<StoreOrCallerResource>,
instance_resource: ResourceArc<InstanceResource>,
global_name: String,
) -> NifResult<Term> {
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_set_global_value", schedule = "DirtyCpu")]
pub fn set_global_value(
env: rustler::Env,
store_or_caller_resource: ResourceArc<StoreOrCallerResource>,
instance_resource: ResourceArc<InstanceResource>,
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<StoreOrCallerResource>,
Expand Down Expand Up @@ -225,6 +316,36 @@ pub enum WasmValue {
F64(f64),
}

fn decode_term_as_wasm_value(expected_type: ValType, term: Term) -> Option<WasmValue> {
RoyalIcing marked this conversation as resolved.
Show resolved Hide resolved
let value = match (expected_type, term.get_type()) {
(ValType::I32, TermType::Integer | TermType::Float) => match term.decode::<i32>() {
Ok(value) => WasmValue::I32(value),
Err(_) => return None,
},
(ValType::I64, TermType::Integer | TermType::Float) => match term.decode::<i64>() {
Ok(value) => WasmValue::I64(value),
Err(_) => return None,
},
(ValType::F32, TermType::Integer | TermType::Float) => match term.decode::<f32>() {
Ok(value) => {
if value.is_finite() {
WasmValue::F32(value)
} else {
return None;
}
}
Err(_) => return None,
},
(ValType::F64, TermType::Integer | TermType::Float) => match term.decode::<f64>() {
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<Term>,
Expand Down
2 changes: 2 additions & 0 deletions native/wasmex/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ rustler::init! {
[
engine::new,
engine::precompile_module,
instance::get_global_value,
instance::set_global_value,
instance::call_exported_function,
instance::function_export_exists,
instance::new,
Expand Down
7 changes: 7 additions & 0 deletions test/example_wasm_files/globals.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
(module
(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 "bad_pi_32") (mut f32) (f32.const 0))
(global (export "bad_pi_64") (mut f64) (f64.const 0))
tessi marked this conversation as resolved.
Show resolved Hide resolved
)
51 changes: 51 additions & 0 deletions test/wasmex/instance_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,55 @@ defmodule Wasmex.InstanceTest do
{:ok, %Wasmex.Memory{resource: _}} = Wasmex.Instance.memory(store, instance)
end
end

describe "globals" do
setup do
source = File.read!("#{Path.dirname(__ENV__.file)}/../example_wasm_files/globals.wat")
{:ok, store} = Wasmex.Store.new()
{:ok, module} = Wasmex.Module.compile(store, source)
{:ok, instance} = Wasmex.Instance.new(store, module, %{})
%{instance: instance, store: store}
end

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.get_global_value(store, instance, "unknown_global")

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")
RoyalIcing marked this conversation as resolved.
Show resolved Hide resolved
end

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.set_global_value(store, instance, "unknown_global", 0)

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_32", 99)
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 {: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,
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,
elem(Wasmex.Instance.get_global_value(store, instance, "bad_pi_64"), 1),
0.01
end
end
end
Loading