Given the following extern.wat
file:
(module $extern
(import "sausage" "fresh"
(func $fresh (param i32) (result externref)))
(import "sausage" "get_i32r"
(func $get (param externref) (result i32)))
(import "sausage" "set_i32r"
(func $set (param externref) (param i32)))
(import "sausage" "print_i32"
(func $print_i32 (param i32)))
(func $start (local $ref externref)
;; let ref = fresh 42
(local.set $ref (call $fresh (i32.const 42)))
;; print_i32 (get ref)
(call $print_i32 (call $get (local.get $ref)))
;; set ref 13
(call $set (local.get $ref) (i32.const 13) )
;; print_i32 (get ref)
(call $print_i32 (call $get (local.get $ref)))
)
(start $start)
)
You can define the various required external functions in OCaml like this :
open Owi
(* an extern module that will be linked with a wasm module *)
let extern_module : Concrete_value.Func.extern_func Link.extern_module =
(* some custom functions *)
let rint : int32 ref Type.Id.t = Type.Id.make () in
let fresh i = ref i in
let set r (i : int32) = r := i in
let get r : int32 = !r in
let print_i32 (i : int32) = Printf.printf "%li\n%!" i in
(* we need to describe their types *)
let functions =
[ ( "print_i32"
, Concrete_value.Func.Extern_func (Func (Arg (I32, Res), R0), print_i32)
)
; ( "fresh"
, Concrete_value.Func.Extern_func
(Func (Arg (I32, Res), R1 (Externref rint)), fresh) )
; ( "set_i32r"
, Concrete_value.Func.Extern_func
(Func (Arg (Externref rint, Arg (I32, Res)), R0), set) )
; ( "get_i32r"
, Concrete_value.Func.Extern_func
(Func (Arg (Externref rint, Res), R1 I32), get) )
]
in
{ functions }
(* a link state that contains our custom module, available under the name `sausage` *)
let link_state =
Link.extern_module Link.empty_state ~name:"sausage" extern_module
(* a pure wasm module refering to `sausage` *)
let pure_wasm_module =
match Parse.Text.Module.from_file (Fpath.v "extern.wat") with
| Error _ -> assert false
| Ok modul -> modul
(* our pure wasm module, linked with `sausage` *)
let module_to_run, link_state =
match
Compile.Text.until_link link_state ~unsafe:false ~rac:false ~srac:false
~optimize:true ~name:None pure_wasm_module
with
| Error _ -> assert false
| Ok v -> v
(* let's run it ! it will print the values as defined in the print_i32 function *)
let () =
match Interpret.Concrete.modul link_state.envs module_to_run with
| Error _ -> assert false
| Ok () -> ()
You'll get the expected result:
$ ./extern.exe
42
13
Owi also allows interacting with linear memory through external functions. This is helpful because it enables the host system to communicate directly with a Wasm instance through its linear memory. Consider the tiny example below to illustrate this:
(module $extern_mem
(import "chorizo" "memset" (func $memset (param i32 i32 i32)))
(import "chorizo" "print_x64" (func $print_x64 (param i64)))
(memory 1)
(func $start
;; memset 0 0xAA 8
(call $memset (i32.const 0) (i32.const 0xAA) (i32.const 8))
;; print_x64 (load 0)
(call $print_x64 (i64.load (i32.const 0)))
)
(start $start)
)
In the module $extern_mem
, we first import $memset
and $print_x64
. Then,
in the $start
function, we initialize the memory starting at address
(i32.const 0)
with a sequence of length (i32.const 8)
with bytes of
(i32.const 0xAA)
.
The definition of the external functions follows the same format as the previous example. The difference is that, now, in the GADT definition of memset to allow the memory to be passed to this function, we need to wrap the three I32 arguments in a Mem variant. That is, instead of writing memset as:
(Func (Arg (I32, (Arg (I32, (Arg (I32, Res))))), R0), memset)
One should use:
(Func (Mem (Arg (I32, (Arg (I32, (Arg (I32, Res)))))), R0), memset)
See the module below for the whole implementation:
open Owi
(* an extern module that will be linked with a wasm module *)
let extern_module : Concrete_value.Func.extern_func Link.extern_module =
(* some custom functions *)
let memset m start byte length =
let rec loop offset =
if Int32.le offset length then begin
Concrete_memory.store_8 m ~addr:(Int32.add start offset) byte;
loop (Int32.add offset 1l)
end
in
loop 0l
in
let print_x64 (i : int64) = Printf.printf "0x%LX\n%!" i in
(* we need to describe their types *)
let functions =
[ ( "print_x64"
, Concrete_value.Func.Extern_func (Func (Arg (I64, Res), R0), print_x64)
)
; ( "memset"
, Concrete_value.Func.Extern_func
(Func (Mem (Arg (I32, Arg (I32, Arg (I32, Res)))), R0), memset) )
]
in
{ functions }
(* a link state that contains our custom module, available under the name `chorizo` *)
let link_state =
Link.extern_module Link.empty_state ~name:"chorizo" extern_module
(* a pure wasm module refering to `$extern_mem` *)
let pure_wasm_module =
match Parse.Text.Module.from_file (Fpath.v "extern_mem.wat") with
| Error _ -> assert false
| Ok modul -> modul
(* our pure wasm module, linked with `chorizo` *)
let module_to_run, link_state =
match
Compile.Text.until_link link_state ~unsafe:false ~rac:false ~srac:false
~optimize:true ~name:None pure_wasm_module
with
| Error _ -> assert false
| Ok v -> v
(* let's run it ! it will print the values as defined in the print_i64 function *)
let () =
match Interpret.Concrete.modul link_state.envs module_to_run with
| Error _ -> assert false
| Ok () -> ()
Running the above program should yield:
$ ./extern_mem.exe
0xAAAAAAAAAAAAAAAA
To learn more, see our advanced Game of Life example
based on the famous cellular automaton by Conway. It show how to link several modules from different .wat
files.