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

Properly call host machine functions defined via imports #24

Merged
merged 9 commits into from
Jan 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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