From 1a9f62c016568765c4aca56d7a484c8df030a9ee Mon Sep 17 00:00:00 2001 From: Alex Dovzhanyn Date: Fri, 4 Jan 2019 12:21:08 -0500 Subject: [PATCH 1/7] basic working version of function imports --- lib/decoding/module.ex | 1 + lib/execution/executor.ex | 34 ++++++++++++++++++++++--------- lib/execution/module_instance.ex | 8 +++++--- lib/wasp_vm.ex | 34 ++++++++++++------------------- test/fixtures/wasm/log.wasm | Bin 0 -> 70 bytes test/fixtures/wat/log.wat | 11 ++++++++++ 6 files changed, 54 insertions(+), 34 deletions(-) create mode 100644 test/fixtures/wasm/log.wasm create mode 100644 test/fixtures/wat/log.wat diff --git a/lib/decoding/module.ex b/lib/decoding/module.ex index 2172aa1..69f4c65 100644 --- a/lib/decoding/module.ex +++ b/lib/decoding/module.ex @@ -14,6 +14,7 @@ defmodule WaspVM.Module do elements: [], globals: [], data: [], + resolved_imports: %{}, custom: %{} @moduledoc false diff --git a/lib/execution/executor.ex b/lib/execution/executor.ex index e52b7a0..98cb9d4 100644 --- a/lib/execution/executor.ex +++ b/lib/execution/executor.ex @@ -11,21 +11,35 @@ defmodule WaspVM.Executor do # Reference for tests being used: https://github.com/WebAssembly/wabt/tree/master/test def create_frame_and_execute(vm, addr, stack \\ []) do - {{inputs, _outputs}, module_ref, instr, locals} = elem(vm.store.funcs, addr) + case elem(vm.store.funcs, addr) do + {{inputs, _outputs}, module_ref, instr, locals} -> + {args, stack} = Enum.split(stack, tuple_size(inputs)) - {args, stack} = Enum.split(stack, tuple_size(inputs)) + %{^module_ref => module} = vm.modules - %{^module_ref => module} = vm.modules + frame = %Frame{ + module: module, + instructions: instr, + locals: List.to_tuple(args ++ locals) + } - frame = %Frame{ - module: module, - instructions: instr, - locals: List.to_tuple(args ++ locals) - } + total_instr = map_size(instr) - total_instr = map_size(instr) + execute(frame, vm, stack, total_instr) + {:hostfunc, {inputs, _outputs}, mname, fname, module_ref} -> + {args, stack} = Enum.split(stack, tuple_size(inputs)) - execute(frame, vm, stack, total_instr) + %{^module_ref => module} = vm.modules + + func = + module.resolved_imports + |> Map.get(mname) + |> Map.get(fname) + + apply(func, args) + + {vm, stack} + end end def execute(_frame, vm, stack, total_instr, next_instr) when next_instr >= total_instr or next_instr < 0, do: {vm, stack} diff --git a/lib/execution/module_instance.ex b/lib/execution/module_instance.ex index 47e2d4e..092bc84 100644 --- a/lib/execution/module_instance.ex +++ b/lib/execution/module_instance.ex @@ -11,7 +11,8 @@ defmodule WaspVM.ModuleInstance do memaddrs: [], globaladdrs: [], exports: [], - types: [] + types: [], + resolved_imports: %{} @moduledoc false @@ -59,7 +60,8 @@ defmodule WaspVM.ModuleInstance do memaddrs: memaddrs, funcaddrs: funcaddrs, globaladdrs: globaladdrs, - types: module.types + types: module.types, + resolved_imports: module.resolved_imports }) # Exports need to happen after everything else is initialized @@ -89,7 +91,7 @@ defmodule WaspVM.ModuleInstance do |> Enum.map(fn imp -> type = Enum.at(module.types, imp.index) - {:hostfunc, type, imp.module, imp.field} + {:hostfunc, type, imp.module, imp.field, ref} end) funcs = diff --git a/lib/wasp_vm.ex b/lib/wasp_vm.ex index 6d8814c..f43f2ca 100644 --- a/lib/wasp_vm.ex +++ b/lib/wasp_vm.ex @@ -21,41 +21,31 @@ defmodule WaspVM do def start, do: GenServer.start_link(__MODULE__, []) @doc false - def init(_args) do - { - :ok, - %WaspVM{ - modules: %{}, - store: %Store{} - } - } - end + def init(_args), do: {:ok, %WaspVM{modules: %{}, store: %Store{}}} @doc """ Load a binary WebAssembly file (.wasm) as a module into the VM """ - - @spec load_file(pid, String.t()) :: {:ok, WaspVM.Module} - def load_file(ref, filename) do - GenServer.call(ref, {:load_module, Decoder.decode_file(filename)}, :infinity) + @spec load_file(pid, String.t(), map) :: {:ok, WaspVM.Module} + def load_file(ref, filename, imports \\ %{}) do + GenServer.call(ref, {:load_module, Decoder.decode_file(filename), imports}, :infinity) end @doc """ Load a WebAssembly module directly from a binary into the VM """ - - @spec load(pid, binary) :: {:ok, WaspVM.Module} - def load(ref, binary) when is_binary(binary) do - GenServer.call(ref, {:load_module, Decoder.decode(binary)}, :infinity) + @spec load(pid, binary, map) :: {:ok, WaspVM.Module} + def load(ref, binary, imports \\ %{}) when is_binary(binary) do + GenServer.call(ref, {:load_module, Decoder.decode(binary), imports}, :infinity) end @doc """ Load a module that was already decoded by load/3 or load_file/3. This is useful for caching modules, as it skips the entire decoding step. """ - @spec load_module(pid, WaspVM.Module) :: {:ok, WaspVM.Module} - def load_module(ref, module) do - GenServer.call(ref, {:load_module, module}, :infinity) + @spec load_module(pid, WaspVM.Module, map) :: {:ok, WaspVM.Module} + def load_module(ref, module, imports \\ %{}) do + GenServer.call(ref, {:load_module, module, imports}, :infinity) end @doc """ @@ -73,7 +63,9 @@ defmodule WaspVM do @spec vm_state(pid) :: WaspVM def vm_state(ref), do: GenServer.call(ref, :vm_state, :infinity) - def handle_call({:load_module, module}, _from, vm) do + def handle_call({:load_module, module, imports}, _from, vm) do + module = Map.put(module, :resolved_imports, imports) + {moduleinst, store} = ModuleInstance.instantiate(ModuleInstance.new(), module, vm.store) modules = Map.put(vm.modules, moduleinst.ref, moduleinst) diff --git a/test/fixtures/wasm/log.wasm b/test/fixtures/wasm/log.wasm new file mode 100644 index 0000000000000000000000000000000000000000..0695b58e8a8772e4b3cd1beb4d48d87220ec036c GIT binary patch literal 70 zcmZQbEY4+QU|?Y6VoG4FWdLHvS|%Yz=G440uH^i@;{2RcpZs(N24*HkMs{vS_Vm<} V;KHI3pn7gbP6h=A0U(~p4FKy+4Iuyk literal 0 HcmV?d00001 diff --git a/test/fixtures/wat/log.wat b/test/fixtures/wat/log.wat new file mode 100644 index 0000000..801c5aa --- /dev/null +++ b/test/fixtures/wat/log.wat @@ -0,0 +1,11 @@ +(module + (import "env" "consoleLog" (func $consoleLog (param f32))) + (export "getSqrt" (func $getSqrt)) + (func $getSqrt (param f32) (result f32) + get_local 0 + call $consoleLog + + get_local 0 + f32.sqrt + ) +) From 030c1318f474a20da5aae7cbcb1dfc720a0a1bbf Mon Sep 17 00:00:00 2001 From: Alex Dovzhanyn Date: Fri, 4 Jan 2019 12:43:07 -0500 Subject: [PATCH 2/7] host funcs can have a return value + fix host func ordering --- lib/execution/executor.ex | 8 ++++++-- lib/execution/module_instance.ex | 1 + test/fixtures/wasm/log.wasm | Bin 70 -> 92 bytes test/fixtures/wat/log.wat | 6 ++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/execution/executor.ex b/lib/execution/executor.ex index 98cb9d4..9e7d95c 100644 --- a/lib/execution/executor.ex +++ b/lib/execution/executor.ex @@ -36,9 +36,13 @@ defmodule WaspVM.Executor do |> Map.get(mname) |> Map.get(fname) - apply(func, args) + return_val = apply(func, args) - {vm, stack} + if !is_number(return_val) do + {vm, stack} + else + {vm, [return_val | stack]} + end end end diff --git a/lib/execution/module_instance.ex b/lib/execution/module_instance.ex index 092bc84..a77adb6 100644 --- a/lib/execution/module_instance.ex +++ b/lib/execution/module_instance.ex @@ -88,6 +88,7 @@ defmodule WaspVM.ModuleInstance do host_funcs = module.imports |> Enum.filter(& &1.type == :typeidx) + |> Enum.sort(& &1.index <= &2.index) |> Enum.map(fn imp -> type = Enum.at(module.types, imp.index) diff --git a/test/fixtures/wasm/log.wasm b/test/fixtures/wasm/log.wasm index 0695b58e8a8772e4b3cd1beb4d48d87220ec036c..cf0e8d8a21bfd501ec15f3c8eab8c645303b7a31 100644 GIT binary patch literal 92 zcmZQbEY4+QU|?Y6VoG4FWvm4f3``14EV+p#8ElCuDTW3H3=GVvd1YM5`FX|pIjKJR p=?sj_OpFZd+>Gq$sU^XMMI{VOT*8ck3 Date: Mon, 7 Jan 2019 20:32:54 -0500 Subject: [PATCH 3/7] makes host funcs work with gas --- lib/execution/executor.ex | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/execution/executor.ex b/lib/execution/executor.ex index 2ab8194..a052baf 100644 --- a/lib/execution/executor.ex +++ b/lib/execution/executor.ex @@ -26,8 +26,11 @@ defmodule WaspVM.Executor do total_instr = map_size(instr) - execute(frame, vm, stack, total_instr) + execute(frame, vm, gas, stack, total_instr, gas_limit) {:hostfunc, {inputs, _outputs}, mname, fname, module_ref} -> + # TODO: How should we handle gas for host functions? Does gas price get passed in? + # Do we default to a gas value? + {args, stack} = Enum.split(stack, tuple_size(inputs)) %{^module_ref => module} = vm.modules @@ -39,10 +42,12 @@ defmodule WaspVM.Executor do return_val = apply(func, args) + # TODO: Gas needs to be updated based on the comment above instead of + # just getting passed through if !is_number(return_val) do - {vm, stack} + {vm, gas, stack} else - {vm, [return_val | stack]} + {vm, gas, [return_val | stack]} end end end @@ -885,7 +890,7 @@ defmodule WaspVM.Executor do bin_size = Enum.count(bin) target = 32 - bin_size - shift - zero_leading_map = Enum.map(1..target, fn x -> 1 end) + zero_leading_map = Enum.map(1..target, fn -> 1 end) Integer.undigits(zero_leading_map ++ bin, 2) end From dfc9e1109a4f04d678558aa08cf91e09097e5449 Mon Sep 17 00:00:00 2001 From: Alex Dovzhanyn Date: Mon, 7 Jan 2019 20:40:09 -0500 Subject: [PATCH 4/7] fix a few tests --- lib/execution/executor.ex | 6 +----- test/program_test.exs | 13 ------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/lib/execution/executor.ex b/lib/execution/executor.ex index a052baf..3598b91 100644 --- a/lib/execution/executor.ex +++ b/lib/execution/executor.ex @@ -890,12 +890,8 @@ defmodule WaspVM.Executor do bin_size = Enum.count(bin) target = 32 - bin_size - shift - zero_leading_map = Enum.map(1..target, fn -> 1 end) + zero_leading_map = Enum.map(1..target, fn _ -> 1 end) Integer.undigits(zero_leading_map ++ bin, 2) end - - - - end diff --git a/test/program_test.exs b/test/program_test.exs index dba77fd..8f8757f 100644 --- a/test/program_test.exs +++ b/test/program_test.exs @@ -14,17 +14,4 @@ defmodule WaspVM.ProgramTest do assert result_2 == 2 end - test "atom program works properly" do - {:ok, pid} = WaspVM.start() - WaspVM.load_file(pid, "test/fixtures/wasm/AtomVM.wasm") |> IO.inspect - #{status, result_1} = WaspVM.execute(pid, "main", [-4]) - #{status, result_2} = WaspVM.execute(pid, "main", [4]) - - #assert result_1 == -2 - # assert result_2 == 2 - end - - - - end From c5578dfa7ac9561a7955d54cc168ea9d47558c5c Mon Sep 17 00:00:00 2001 From: Alex Dovzhanyn Date: Tue, 8 Jan 2019 15:50:27 -0500 Subject: [PATCH 5/7] update readme to reflect gas output --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c1ce61..ea2557c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ the [Elixium Network](https://www.elixiumnetwork.org) {:ok, ref} = WaspVM.start() # Start WaspVM WaspVM.load_file(ref, "path/to/wasm/file.wasm") # Load a module WaspVM.execute(ref, "some_exported_function") # Call a function -# => {:ok, :function_return_value} +# => {:ok, total_gas_cost, :function_return_value} ``` ## Installation From 069caa8838fc8dad76625210c99a5f1de1bfa53f Mon Sep 17 00:00:00 2001 From: Alex Dovzhanyn Date: Tue, 8 Jan 2019 16:49:02 -0500 Subject: [PATCH 6/7] improve documentation --- lib/decoding/name_section_parser.ex | 2 + lib/wasp_vm.ex | 64 ++++++++++++++++++++++++++++ test/fixtures/wasm/addition.wasm | Bin 0 -> 47 bytes test/fixtures/wat/addition.wat | 7 +++ 4 files changed, 73 insertions(+) create mode 100644 test/fixtures/wasm/addition.wasm create mode 100644 test/fixtures/wat/addition.wat diff --git a/lib/decoding/name_section_parser.ex b/lib/decoding/name_section_parser.ex index 3b2cec2..c160f89 100644 --- a/lib/decoding/name_section_parser.ex +++ b/lib/decoding/name_section_parser.ex @@ -1,6 +1,8 @@ defmodule WaspVM.Decoder.NameSectionParser do alias WaspVM.LEB128 + @moduledoc false + def parse(binary) do binary |> parse_subsections() diff --git a/lib/wasp_vm.ex b/lib/wasp_vm.ex index 70dcf3f..d84c476 100644 --- a/lib/wasp_vm.ex +++ b/lib/wasp_vm.ex @@ -51,6 +51,70 @@ defmodule WaspVM do @doc """ Call an exported function by name from the VM. The function must have been loaded in through a module using load_file/2 or load/2 previously + + ## Usage + ### Most basic usage for a simple module (no imports or host functions): + + #### Wasm File (add.wat) + ``` + (module + (func (export "basic_add") (param i32 i32) (result i32) + get_local 0 + get_local 1 + i32.add + ) + ) + ``` + Use an external tool to compile add.wat to add.wasm (compile from text + representation to binary representation) + + {:ok, pid} = WaspVM.start() # Start the VM + WaspVM.load_file(pid, "path/to/add.wasm") # Load the module that contains our add function + + # Call the add function, passing in 3 and 10 as args + {:ok, gas, result} = WaspVM.execute(pid, "basic_add", [3, 10]) + + ### Executing modules with host functions: + + #### Wasm file (log.wat) + ``` + (module + (import "env" "consoleLog" (func $consoleLog (param f32))) + (export "getSqrt" (func $getSqrt)) + (func $getSqrt (param f32) (result f32) + get_local 0 + f32.sqrt + tee_local 0 + call $consoleLog + + get_local 0 + ) + ) + ``` + Use an external tool to compile log.wat to log.wasm (compile from text + representation to binary representation) + + {:ok, pid} = WaspVM.start() # Start the VM + + # Define the imports used in this module. Keys in the import map + # must be strings + imports = %{ + "env" => %{ + "consoleLog" => fn x -> IO.puts "its \#{x}" end + } + } + + # Load the file, passing in the imports + WaspVM.load_file(pid, "path/to/log.wasm", imports) + + # Call getSqrt with an argument of 25 + WaspVM.execute(pid, "getSqrt", [25]) + + Program execution can also be limited by specifying a `:gas_limit` option: + + WaspVM.execute(pid, "some_func", [], gas_limit: 100) + + This will stop execution of the program if the accumulated gas exceeds 100 """ @spec execute(pid, String.t(), list, list) :: :ok | {:ok, any} | {:error, any} def execute(ref, func, args \\ [], opts \\ []) do diff --git a/test/fixtures/wasm/addition.wasm b/test/fixtures/wasm/addition.wasm new file mode 100644 index 0000000000000000000000000000000000000000..907f6ba48de32e191dc67aec2949be4aad2d3f9e GIT binary patch literal 47 zcmZQbEY4+QU|?WmXG~zKuV<`hW@2Pu=VjzfN-WMyj!#TUVPN3mWMpShU{GMp;syY} CBL}Mh literal 0 HcmV?d00001 diff --git a/test/fixtures/wat/addition.wat b/test/fixtures/wat/addition.wat new file mode 100644 index 0000000..7defe62 --- /dev/null +++ b/test/fixtures/wat/addition.wat @@ -0,0 +1,7 @@ +(module + (func (export "basic_add") (param i32 i32) (result i32) + get_local 0 + get_local 1 + i32.add + ) +) From dd72cb7aaed10479491bab57d2e07c52f2d2a508 Mon Sep 17 00:00:00 2001 From: fantypants <8563615@gmail.com> Date: Tue, 8 Jan 2019 18:05:37 -0800 Subject: [PATCH 7/7] Deleted Test - Not needed anymore --- test/program_test.exs | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 test/program_test.exs diff --git a/test/program_test.exs b/test/program_test.exs deleted file mode 100644 index 8a24835..0000000 --- a/test/program_test.exs +++ /dev/null @@ -1,20 +0,0 @@ -defmodule WaspVM.ProgramTest do - use ExUnit.Case - require Bitwise - doctest WaspVM - - - test "32 bit div program works properly" do - {:ok, pid} = WaspVM.start() - WaspVM.load_file(pid, "test/fixtures/wasm/int_div.wasm") |> IO.inspect - {status, result_1} = WaspVM.execute(pid, "main", [-4]) - {status, result_2} = WaspVM.execute(pid, "main", [4]) - - assert result_1 == -2 - assert result_2 == 2 - end - - - - -end