From 2c4ac3e6a1bd0de61f65771e0231e69d696ddf22 Mon Sep 17 00:00:00 2001 From: Munjal A Patel <2717686+munjalpatel@users.noreply.github.com> Date: Fri, 5 Jul 2024 04:16:24 -0700 Subject: [PATCH] add ability to dynamically link wasm modules (#596) * add wasm-link test modules and setup * (wip) pass modules to link from elixir to rust * add ability to dynamically link wasm modules * add support to dynamically link compiled module * update linking api to use map-of-map instead of list-of-map * update param name from links to linked_module to avoid confusion with linker * update dynamic-linking docs with additional notes * update api to prevent over exposure of rust resources * rename remaining links to linked_modules * add unit tests for dynamic module linking * add dynamic module linking to the changelog * update error messages returned from rust for clarity * refactor dynamic module linking code to improve readability and maintainability (#1) * update dynamic module linking docs Co-authored-by: Philipp Tessenow * add missing new line at the end of the file Co-authored-by: Philipp Tessenow * add support for dynamically linked module dependencies (#2) * fix nested dynamic link issue * add test for compiled dynamically linked module deps --------- Co-authored-by: Sonny Scroggin Co-authored-by: Philipp Tessenow --- .gitignore | 2 + CHANGELOG.md | 1 + lib/wasmex.ex | 102 +++++++++++++++++++++++++++-- lib/wasmex/instance.ex | 25 +++++-- lib/wasmex/native.ex | 2 +- native/wasmex/src/environment.rs | 31 ++++++++- native/wasmex/src/instance.rs | 18 +++-- test/test_helper.exs | 53 +++++++++++++++ test/wasm_link_dep_test/Cargo.lock | 7 ++ test/wasm_link_dep_test/Cargo.toml | 9 +++ test/wasm_link_dep_test/src/lib.rs | 10 +++ test/wasm_link_test.exs | 71 ++++++++++++++++++++ test/wasm_link_test/Cargo.lock | 7 ++ test/wasm_link_test/Cargo.toml | 9 +++ test/wasm_link_test/src/lib.rs | 15 +++++ 15 files changed, 344 insertions(+), 18 deletions(-) create mode 100644 test/wasm_link_dep_test/Cargo.lock create mode 100644 test/wasm_link_dep_test/Cargo.toml create mode 100644 test/wasm_link_dep_test/src/lib.rs create mode 100644 test/wasm_link_test.exs create mode 100644 test/wasm_link_test/Cargo.lock create mode 100644 test/wasm_link_test/Cargo.toml create mode 100644 test/wasm_link_test/src/lib.rs diff --git a/.gitignore b/.gitignore index 3946b4cd..a5c5a3b9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ wasmex-*.tar # Cargo things in the Rust part of this package priv/native/libwasmex.so test/wasm_source/target/* +test/wasm_link_test/target/* +test/wasm_link_dep_test/target/* .mix_tasks **/.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 340a1534..8029af20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,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](https://github.com/tessi/wasmex/pull/540)) +- ability to dynamically link wasm modules ([#596](https://github.com/tessi/wasmex/pull/596)) ### Removed diff --git a/lib/wasmex.ex b/lib/wasmex.ex index 79ece608..a3c3aa7e 100644 --- a/lib/wasmex.ex +++ b/lib/wasmex.ex @@ -92,6 +92,55 @@ defmodule Wasmex do - `:f32` a 32 bit float - `:f64` a 64 bit float + ### Linking multiple Wasm modules + + Wasm module `links` may be given as an additional option. + Links is a map of module names to Wasm modules. + + iex> calculator_wasm = File.read!(TestHelper.wasm_link_test_file_path()) + iex> utils_wasm = File.read!(TestHelper.wasm_test_file_path()) + iex> links = %{utils: %{bytes: utils_wasm}} + iex> {:ok, pid} = Wasmex.start_link(%{bytes: calculator_wasm, links: links}) + iex> Wasmex.call_function(pid, "sum_range", [1, 5]) + {:ok, [15]} + + It is also possible to link an already compiled module. + This improves performance if the same module is used many times by compiling it only once. + + iex> calculator_wasm = File.read!(TestHelper.wasm_link_test_file_path()) + iex> utils_wasm = File.read!(TestHelper.wasm_test_file_path()) + iex> {:ok, store} = Wasmex.Store.new() + iex> {:ok, utils_module} = Wasmex.Module.compile(store, utils_wasm) + iex> links = %{utils: %{module: utils_module}} + iex> {:ok, pid} = Wasmex.start_link(%{bytes: calculator_wasm, links: links, store: store}) + iex> Wasmex.call_function(pid, "sum_range", [1, 5]) + {:ok, [15]} + + **Important:** Make sure to use the same store for the linked modules and the main module. + + When linking multiple Wasm modules, it is important to handle their dependencies properly. + This can be achieved by providing a map of module names to their respective Wasm modules in the `links` option. + + For example, if we have a main module that depends on a calculator module, and the calculator module depends on a utils module, we can link them as follows: + + iex> main_wasm = File.read!(TestHelper.wasm_link_dep_test_file_path()) + iex> calculator_wasm = File.read!(TestHelper.wasm_link_test_file_path()) + iex> utils_wasm = File.read!(TestHelper.wasm_test_file_path()) + iex> links = %{ + ...> calculator: %{ + ...> bytes: calculator_wasm, + ...> links: %{ + ...> utils: %{bytes: utils_wasm} + ...> } + ...> } + ...> } + iex> {:ok, pid} = Wasmex.start_link(%{bytes: main_wasm, links: links}) + + In this example, the `links` map specifies that the `calculator` module depends on the `utils` module. + The `links` map is a nested map, where each module name is associated with a map that contains the Wasm module bytes and its dependencies. + + The `links` map can also be used to link an already compiled module, as shown in the previous examples. + ### WASI Optionally, modules can be run with WebAssembly System Interface (WASI) support. @@ -159,6 +208,9 @@ defmodule Wasmex do def start_link(%{} = opts) when not is_map_key(opts, :imports), do: start_link(Map.merge(opts, %{imports: %{}})) + def start_link(%{} = opts) when not is_map_key(opts, :links), + do: start_link(Map.merge(opts, %{links: %{}})) + def start_link(%{} = opts) when is_map_key(opts, :module) and not is_map_key(opts, :store), do: {:error, :must_specify_store_used_to_compile_module} @@ -182,15 +234,41 @@ defmodule Wasmex do end end - def start_link(%{store: store, module: module, imports: imports} = opts) - when is_map(imports) and not is_map_key(opts, :bytes) do + def start_link(%{links: links, store: store} = opts) + when is_map(links) and not is_map_key(opts, :compiled_links) do + compiled_links = + links + |> flatten_links() + |> Enum.reverse() + |> Enum.uniq_by(&elem(&1, 0)) + |> Enum.map(&build_compiled_links(&1, store)) + + opts + |> Map.delete(:links) + |> Map.put(:compiled_links, compiled_links) + |> start_link() + end + + def start_link(%{store: store, module: module, imports: imports, compiled_links: links} = opts) + when is_map(imports) and is_list(links) and not is_map_key(opts, :bytes) do GenServer.start_link(__MODULE__, %{ store: store, module: module, + links: links, imports: stringify_keys(imports) }) end + defp flatten_links(links) do + Enum.flat_map(links, fn {name, opts} -> + if Map.has_key?(opts, :links) do + [{name, Map.drop(opts, [:links])} | flatten_links(opts.links)] + else + [{name, opts}] + end + end) + end + defp build_store(opts) do if Map.has_key?(opts, :wasi) do Wasmex.Store.new_wasi(stringify_keys(opts[:wasi])) @@ -199,6 +277,17 @@ defmodule Wasmex do end end + defp build_compiled_links({name, %{bytes: bytes} = opts}, store) + when not is_map_key(opts, :module) do + with {:ok, module} <- Wasmex.Module.compile(store, bytes) do + %{name: stringify(name), module: module} + end + end + + defp build_compiled_links({name, %{module: module}}, _store) do + %{name: stringify(name), module: module} + end + @doc ~S""" Returns whether a function export with the given `name` exists in the Wasm instance. @@ -353,6 +442,10 @@ defmodule Wasmex do for {key, val} <- map, into: %{}, do: {stringify(key), stringify_keys(val)} end + defp stringify_keys(list) when is_list(list) do + for val <- list, into: [], do: stringify_keys(val) + end + defp stringify_keys(value), do: value defp stringify(s) when is_binary(s), do: s @@ -361,8 +454,9 @@ defmodule Wasmex do # Server @impl true - def init(%{store: store, module: module, imports: imports} = state) when is_map(imports) do - case Wasmex.Instance.new(store, module, imports) do + def init(%{store: store, module: module, imports: imports, links: links} = state) + when is_map(imports) and is_list(links) do + case Wasmex.Instance.new(store, module, imports, links) do {:ok, instance} -> {:ok, Map.merge(state, %{instance: instance})} {:error, reason} -> {:error, reason} end diff --git a/lib/wasmex/instance.ex b/lib/wasmex/instance.ex index bf90b12e..daf5b29f 100644 --- a/lib/wasmex/instance.ex +++ b/lib/wasmex/instance.ex @@ -35,6 +35,8 @@ defmodule Wasmex.Instance do The `import` parameter is a nested map of Wasm namespaces. Each namespace consists of a name and a map of function names to function signatures. + The `links` parameter is a list of name-module pairs that are dynamically linked to the instance. + Function signatures are a tuple of the form `{:fn, arg_types, return_types, callback}`. Where `arg_types` and `return_types` are lists of `:i32`, `:i64`, `:f32`, `:f64`. @@ -61,17 +63,28 @@ defmodule Wasmex.Instance do ...> "imported_void" => {:fn, [], [], fn _context -> nil end} ...> } ...> } - iex> {:ok, %Wasmex.Instance{}} = Wasmex.Instance.new(store, module, imports) + ...> links = [] + iex> {:ok, %Wasmex.Instance{}} = Wasmex.Instance.new(store, module, imports, links) """ - @spec new(Wasmex.StoreOrCaller.t(), Wasmex.Module.t(), %{ - optional(binary()) => (... -> any()) - }) :: + @spec new( + Wasmex.StoreOrCaller.t(), + Wasmex.Module.t(), + %{optional(binary()) => (... -> any())}, + [%{optional(binary()) => Wasmex.Module.t()}] | [] + ) :: {:ok, __MODULE__.t()} | {:error, binary()} - def new(store_or_caller, module, imports) when is_map(imports) do + def new(store_or_caller, module, imports, links \\ []) + when is_map(imports) and is_list(links) do %Wasmex.StoreOrCaller{resource: store_or_caller_resource} = store_or_caller %Wasmex.Module{resource: module_resource} = module - case Wasmex.Native.instance_new(store_or_caller_resource, module_resource, imports) do + links = + links + |> Enum.map(fn %{name: name, module: module} -> + %{name: name, module_resource: module.resource} + end) + + case Wasmex.Native.instance_new(store_or_caller_resource, module_resource, imports, links) do {:error, err} -> {:error, err} resource -> {:ok, __wrap_resource__(resource)} end diff --git a/lib/wasmex/native.ex b/lib/wasmex/native.ex index 6426be89..cdf62e75 100644 --- a/lib/wasmex/native.ex +++ b/lib/wasmex/native.ex @@ -32,7 +32,7 @@ defmodule Wasmex.Native do def module_serialize(_module_resource), do: error() def module_unsafe_deserialize(_binary, _engine_resource), do: error() - def instance_new(_store_or_caller_resource, _module_resource, _imports), do: error() + def instance_new(_store_or_caller_resource, _module_resource, _imports, _links), do: error() def instance_function_export_exists( _store_or_caller_resource, diff --git a/native/wasmex/src/environment.rs b/native/wasmex/src/environment.rs index 9d7cffcf..4b5fcf21 100644 --- a/native/wasmex/src/environment.rs +++ b/native/wasmex/src/environment.rs @@ -8,9 +8,9 @@ use wasmtime::{Caller, FuncType, Linker, Val, ValType}; use wiggle::anyhow::{self, anyhow}; use crate::{ - atoms::{self}, + atoms, caller::{remove_caller, set_caller}, - instance::{map_wasm_values_to_vals, WasmValue}, + instance::{map_wasm_values_to_vals, LinkedModule, WasmValue}, memory::MemoryResource, store::{StoreData, StoreOrCaller, StoreOrCallerResource}, }; @@ -25,6 +25,33 @@ pub struct CallbackToken { pub return_values: Mutex)>>, } +pub fn link_modules( + linker: &mut Linker, + store: &mut StoreOrCaller, + linked_modules: Vec, +) -> Result<(), Error> { + for linked_module in linked_modules { + let module_name = linked_module.name; + let module = linked_module.module_resource.inner.lock().map_err(|e| { + rustler::Error::Term(Box::new(format!( + "Could not unlock linked module resource as the mutex was poisoned: {e}" + ))) + })?; + + let instance = linker.instantiate(&mut *store, &module).map_err(|e| { + rustler::Error::Term(Box::new(format!( + "Could not instantiate linked module: {e}" + ))) + })?; + + linker + .instance(&mut *store, &module_name, instance) + .map_err(|err| Error::Term(Box::new(err.to_string())))?; + } + + Ok(()) +} + pub fn link_imports(linker: &mut Linker, imports: MapIterator) -> Result<(), Error> { for (namespace_name, namespace_definition) in imports { let namespace_name = namespace_name.decode::()?; diff --git a/native/wasmex/src/instance.rs b/native/wasmex/src/instance.rs index ba12f341..cd1b16d1 100644 --- a/native/wasmex/src/instance.rs +++ b/native/wasmex/src/instance.rs @@ -2,9 +2,8 @@ use rustler::{ dynamic::TermType, env::{OwnedEnv, SavedTerm}, resource::ResourceArc, - types::tuple::make_tuple, - types::ListIterator, - Encoder, Env as RustlerEnv, Error, MapIterator, NifResult, Term, + types::{tuple::make_tuple, ListIterator}, + Encoder, Env as RustlerEnv, Error, MapIterator, NifMap, NifResult, Term, }; use std::ops::Deref; use std::sync::Mutex; @@ -14,13 +13,19 @@ use wasmtime::{Instance, Linker, Module, Val, ValType}; use crate::{ atoms, - environment::{link_imports, CallbackTokenResource}, + environment::{link_imports, link_modules, CallbackTokenResource}, functions, module::ModuleResource, printable_term_type::PrintableTermType, store::{StoreData, StoreOrCaller, StoreOrCallerResource}, }; +#[derive(NifMap)] +pub struct LinkedModule { + pub name: String, + pub module_resource: ResourceArc, +} + pub struct InstanceResource { pub inner: Mutex, } @@ -37,6 +42,7 @@ pub fn new( store_or_caller_resource: ResourceArc, module_resource: ResourceArc, imports: MapIterator, + linked_modules: Vec, ) -> Result, rustler::Error> { let module = module_resource.inner.lock().map_err(|e| { rustler::Error::Term(Box::new(format!( @@ -50,7 +56,7 @@ pub fn new( ))) })?); - let instance = link_and_create_instance(store_or_caller, &module, imports)?; + let instance = link_and_create_instance(store_or_caller, &module, imports, linked_modules)?; let resource = ResourceArc::new(InstanceResource { inner: Mutex::new(instance), }); @@ -61,6 +67,7 @@ fn link_and_create_instance( store_or_caller: &mut StoreOrCaller, module: &Module, imports: MapIterator, + linked_modules: Vec, ) -> Result { let mut linker = Linker::new(store_or_caller.engine()); if let Some(_wasi_ctx) = &store_or_caller.data().wasi { @@ -68,6 +75,7 @@ fn link_and_create_instance( wasmtime_wasi::add_to_linker(&mut linker, |s: &mut StoreData| s.wasi.as_mut().unwrap()) .map_err(|err| Error::Term(Box::new(err.to_string())))?; } + link_modules(&mut linker, store_or_caller, linked_modules)?; link_imports(&mut linker, imports)?; linker .instantiate(store_or_caller, module) diff --git a/test/test_helper.exs b/test/test_helper.exs index 2a7e5e1b..bf6623aa 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,11 +1,20 @@ defmodule TestHelper do @wasm_test_source_dir "#{Path.dirname(__ENV__.file)}/wasm_test" + @wasm_link_test_source_dir "#{Path.dirname(__ENV__.file)}/wasm_link_test" + @wasm_link_dep_test_source_dir "#{Path.dirname(__ENV__.file)}/wasm_link_dep_test" @wasm_import_test_source_dir "#{Path.dirname(__ENV__.file)}/wasm_import_test" @wasi_test_source_dir "#{Path.dirname(__ENV__.file)}/wasi_test" def wasm_test_file_path, do: "#{@wasm_test_source_dir}/target/wasm32-unknown-unknown/debug/wasmex_test.wasm" + def wasm_link_test_file_path, + do: "#{@wasm_link_test_source_dir}/target/wasm32-unknown-unknown/debug/wasmex_link_test.wasm" + + def wasm_link_dep_test_file_path, + do: + "#{@wasm_link_dep_test_source_dir}/target/wasm32-unknown-unknown/debug/wasmex_link_dep_test.wasm" + def wasm_import_test_file_path, do: "#{@wasm_import_test_source_dir}/target/wasm32-unknown-unknown/debug/wasmex_test.wasm" @@ -16,6 +25,32 @@ defmodule TestHelper do {"", 0} = System.cmd("cargo", ["build"], cd: @wasm_test_source_dir) {"", 0} = System.cmd("cargo", ["build"], cd: @wasm_import_test_source_dir) {"", 0} = System.cmd("cargo", ["build"], cd: @wasi_test_source_dir) + + {"", 0} = + System.cmd( + "cargo", + [ + "rustc", + "--target=wasm32-unknown-unknown", + "--", + "--extern", + "utils=../wasm_test/target/wasm32-unknown-unknown/debug/wasmex_test.wasm" + ], + cd: @wasm_link_test_source_dir + ) + + {"", 0} = + System.cmd( + "cargo", + [ + "rustc", + "--target=wasm32-unknown-unknown", + "--", + "--extern", + "calculator=../wasm_link_test/target/wasm32-unknown-unknown/debug/wasmex_link_test.wasm" + ], + cd: @wasm_link_dep_test_source_dir + ) end def wasm_module do @@ -27,6 +62,24 @@ defmodule TestHelper do %{store: store, module: wasm_module} end + def wasm_link_module do + {:ok, store} = Wasmex.Store.new() + + {:ok, wasm_module} = + Wasmex.Module.compile(store, File.read!(TestHelper.wasm_link_test_file_path())) + + %{store: store, module: wasm_module} + end + + def wasm_link_dep_module do + {:ok, store} = Wasmex.Store.new() + + {:ok, wasm_module} = + Wasmex.Module.compile(store, File.read!(TestHelper.wasm_link_dep_test_file_path())) + + %{store: store, module: wasm_module} + end + def wasm_import_module do {:ok, store} = Wasmex.Store.new() diff --git a/test/wasm_link_dep_test/Cargo.lock b/test/wasm_link_dep_test/Cargo.lock new file mode 100644 index 00000000..d2ef6268 --- /dev/null +++ b/test/wasm_link_dep_test/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "wasmex_link_dep_test" +version = "0.1.0" diff --git a/test/wasm_link_dep_test/Cargo.toml b/test/wasm_link_dep_test/Cargo.toml new file mode 100644 index 00000000..dd3845a6 --- /dev/null +++ b/test/wasm_link_dep_test/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "wasmex_link_dep_test" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[lib] +crate-type =["cdylib"] diff --git a/test/wasm_link_dep_test/src/lib.rs b/test/wasm_link_dep_test/src/lib.rs new file mode 100644 index 00000000..9c0387bc --- /dev/null +++ b/test/wasm_link_dep_test/src/lib.rs @@ -0,0 +1,10 @@ +#[link(wasm_import_module = "calculator")] +extern "C" { + fn sum_range(from: i32, to: i32) -> i32; +} + +#[no_mangle] +pub extern "C" fn calc_seq(from: i32, to: i32) -> i32 { + let result = unsafe { sum_range(from, to) }; + result +} diff --git a/test/wasm_link_test.exs b/test/wasm_link_test.exs new file mode 100644 index 00000000..72a1919b --- /dev/null +++ b/test/wasm_link_test.exs @@ -0,0 +1,71 @@ +defmodule WasmLinkTest do + use ExUnit.Case, async: true + + alias TestHelper + + test "linking wasm modules using bytes" do + calculator_wasm = File.read!(TestHelper.wasm_link_test_file_path()) + utils_wasm = File.read!(TestHelper.wasm_test_file_path()) + + links = %{utils: %{bytes: utils_wasm}} + {:ok, pid} = Wasmex.start_link(%{bytes: calculator_wasm, links: links}) + + assert Wasmex.call_function(pid, "sum_range", [1, 5]) == {:ok, [15]} + end + + test "linking wasm modules using compiled module" do + calculator_wasm = File.read!(TestHelper.wasm_link_test_file_path()) + utils_wasm = File.read!(TestHelper.wasm_test_file_path()) + + {:ok, store} = Wasmex.Store.new() + {:ok, utils_module} = Wasmex.Module.compile(store, utils_wasm) + + links = %{utils: %{module: utils_module}} + {:ok, pid} = Wasmex.start_link(%{bytes: calculator_wasm, links: links, store: store}) + + assert Wasmex.call_function(pid, "sum_range", [1, 5]) == {:ok, [15]} + end + + test "linking multiple modules to satisfy dependencies" do + main_wasm = File.read!(TestHelper.wasm_link_dep_test_file_path()) + calculator_wasm = File.read!(TestHelper.wasm_link_test_file_path()) + utils_wasm = File.read!(TestHelper.wasm_test_file_path()) + + links = %{ + calculator: %{ + bytes: calculator_wasm, + links: %{ + utils: %{bytes: utils_wasm} + } + } + } + + {:ok, pid} = Wasmex.start_link(%{bytes: main_wasm, links: links}) + + assert Wasmex.call_function(pid, "calc_seq", [1, 5]) == {:ok, [15]} + end + + test "linking multiple compiled modules to satisfy dependencies" do + main_wasm = File.read!(TestHelper.wasm_link_dep_test_file_path()) + calculator_wasm = File.read!(TestHelper.wasm_link_test_file_path()) + utils_wasm = File.read!(TestHelper.wasm_test_file_path()) + + {:ok, store} = Wasmex.Store.new() + {:ok, utils_module} = Wasmex.Module.compile(store, utils_wasm) + {:ok, calculator_module} = Wasmex.Module.compile(store, calculator_wasm) + {:ok, main_module} = Wasmex.Module.compile(store, main_wasm) + + links = %{ + calculator: %{ + module: calculator_module, + links: %{ + utils: %{module: utils_module} + } + } + } + + {:ok, pid} = Wasmex.start_link(%{module: main_module, links: links, store: store}) + + assert Wasmex.call_function(pid, "calc_seq", [1, 5]) == {:ok, [15]} + end +end diff --git a/test/wasm_link_test/Cargo.lock b/test/wasm_link_test/Cargo.lock new file mode 100644 index 00000000..c8facc61 --- /dev/null +++ b/test/wasm_link_test/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "wasmex_link_test" +version = "0.1.0" diff --git a/test/wasm_link_test/Cargo.toml b/test/wasm_link_test/Cargo.toml new file mode 100644 index 00000000..61226b92 --- /dev/null +++ b/test/wasm_link_test/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "wasmex_link_test" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[lib] +crate-type =["cdylib"] diff --git a/test/wasm_link_test/src/lib.rs b/test/wasm_link_test/src/lib.rs new file mode 100644 index 00000000..3b719298 --- /dev/null +++ b/test/wasm_link_test/src/lib.rs @@ -0,0 +1,15 @@ +#[link(wasm_import_module = "utils")] +extern "C" { + fn sum(a: i32, b: i32) -> i32; +} + +#[no_mangle] +pub extern "C" fn sum_range(from: i32, to: i32) -> i32 { + let mut result = 0; + + for number in from..=to { + result = unsafe { sum(result, number) }; + } + + result +}