Skip to content

Commit

Permalink
Merge pull request #24 from ElixiumNetwork/working-imports
Browse files Browse the repository at this point in the history
Properly call host machine functions defined via imports
  • Loading branch information
fantypants committed Jan 9, 2019
2 parents 8829ba6 + 47c4c12 commit 2c81da3
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 57 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/decoding/module.ex
Expand Up @@ -14,6 +14,7 @@ defmodule WaspVM.Module do
elements: [],
globals: [],
data: [],
resolved_imports: %{},
custom: %{}

@moduledoc false
Expand Down
2 changes: 2 additions & 0 deletions 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()
Expand Down
51 changes: 35 additions & 16 deletions lib/execution/executor.ex
Expand Up @@ -11,22 +11,45 @@ defmodule WaspVM.Executor do
# Reference for tests being used: https://github.com/WebAssembly/wabt/tree/master/test

def create_frame_and_execute(vm, addr, gas_limit, gas \\ 0, 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),
gas_limit: gas_limit
}

frame = %Frame{
module: module,
instructions: instr,
locals: List.to_tuple(args ++ locals),
gas_limit: gas_limit
}
total_instr = map_size(instr)

total_instr = map_size(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?

execute(frame, vm, gas, stack, total_instr, gas_limit)
{args, stack} = Enum.split(stack, tuple_size(inputs))

%{^module_ref => module} = vm.modules

func =
module.resolved_imports
|> Map.get(mname)
|> Map.get(fname)

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, gas, stack}
else
{vm, gas, [return_val | stack]}
end
end
end

# What happens is we pass in the main limit for the gas & the gas_limit,
Expand Down Expand Up @@ -867,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 x -> 1 end)
zero_leading_map = Enum.map(1..target, fn _ -> 1 end)

Integer.undigits(zero_leading_map ++ bin, 2)
end




end
9 changes: 6 additions & 3 deletions lib/execution/module_instance.ex
Expand Up @@ -11,7 +11,8 @@ defmodule WaspVM.ModuleInstance do
memaddrs: [],
globaladdrs: [],
exports: [],
types: []
types: [],
resolved_imports: %{}

@moduledoc false

Expand Down Expand Up @@ -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
Expand All @@ -86,10 +88,11 @@ 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)

{:hostfunc, type, imp.module, imp.field}
{:hostfunc, type, imp.module, imp.field, ref}
end)

funcs =
Expand Down
98 changes: 77 additions & 21 deletions lib/wasp_vm.ex
Expand Up @@ -21,46 +21,100 @@ 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 """
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
Expand All @@ -75,7 +129,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)
Expand Down
Binary file added test/fixtures/wasm/addition.wasm
Binary file not shown.
Binary file added test/fixtures/wasm/log.wasm
Binary file not shown.
7 changes: 7 additions & 0 deletions 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
)
)
17 changes: 17 additions & 0 deletions test/fixtures/wat/log.wat
@@ -0,0 +1,17 @@
(module
(import "math" "add100" (func $add100 (param f32) (result f32)))
(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
call $add100
tee_local 0
call $consoleLog

get_local 0
f32.sqrt
)
)
19 changes: 3 additions & 16 deletions test/program_test.exs
Expand Up @@ -6,25 +6,12 @@ defmodule WaspVM.ProgramTest do

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])
WaspVM.load_file(pid, "test/fixtures/wasm/int_div.wasm")
{status, gas, result_1} = WaspVM.execute(pid, "main", [-4])
{status, gas, result_2} = WaspVM.execute(pid, "main", [4])

assert result_1 == -2
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

0 comments on commit 2c81da3

Please sign in to comment.