Next: , Previous: , Up: Scheme reference   [Contents][Index]


4.12 Foreign function interface

WebAssembly follows the capability security model, which means that modules cannot do much on their own. Wasm modules are guests within a host. They must be given capabilities by the host in order to interact with the outside world. Modules request capabilities by declaring imports, which the host then fills out with concrete implementations at instantiation time. Hoot provides a foreign function interface (FFI) in the (hoot ffi) module to embed these import declarations within Scheme code.

The define-foreign form declares an import with a given type signature (Wasm is statically typed) and defines a procedure for calling it. The FFI takes care of converting Scheme values to Wasm values and vice versa. For example, declaring an import for creating text nodes in a web browser could look like this:

(define-foreign make-text-node
  "document" "createTextNode"
  (ref string) -> (ref extern))

In the above example, the procedure is bound to the variable make-text-node. In the Wasm binary, this import is named “createTextNode” and resides in the “document” namespace of the import table. A Wasm host is expected to satisfy this import by providing a function that accepts one argument, a string, and returns an arbitary host value which may be null.

Note that declaring an import does not do anything to bind that import to an implementation on the host. The Wasm guest cannot grant capabilities unto itself. Furthermore, the host could be any Wasm runtime, so the actual implementation will vary. In the context of a web browser, the JavaScript code that instantiates a module with this import could look like this:

Scheme.load_main("hello.wasm", {}, {
  document: {
    createTextNode: (text) => document.createTextNode(text)
  }
});

And here’s what it might look like when using the Hoot interpreter:

(use-modules (hoot reflect))
(hoot-instantiate (call-with-input-file "hello.wasm" parse-wasm)
                  `(("document" .
                     (("createTextNode" . ,(lambda (str) `(text ,str)))))))

Once defined, make-text-node can be called like any other procedure:

(define hello (make-text-node "Hello, world!"))

Since the return type of make-text-node is (ref extern), the value of hello is an external reference. To check if a value is an external reference, use the external? predicate:

(external? hello) ; => #t

External references may be null, which could indicate failure, a cache miss, etc. To check if an external value is null, use the external-null? predicate:

(external-null? hello) ; => #f

Note that we defined the return type of make-text-node to be (ref extern), not (ref null extern), so hello would never be null in this example.

A large application will likely need to manipulate many different kinds of foreign values. This introduces an opportunity for errors because external? cannot distinguish between them. The solution is to wrap external values using disjoint types. To define such wrapper types, use define-external-type:

(define-external-type <text-node>
  text-node? wrap-text-node unwrap-text-node)

make-text-node could then be reimplemented like this:

(define-foreign %make-text-node
  "document" "createTextNode"
  (ref string) -> (ref extern))

(define (make-text-node str)
  (wrap-text-node (%make-text-node str)))

(define hello (make-text-node "Hello, world!"))

(external? hello)                    ; => #f
(text-node? hello)                   ; => #t
(external? (unwrap-text-node hello)) ; => #t

We’ve now explained the basics of using the FFI. Read below for detailed API documentation.

Syntax: define-foreign scheme-name namespace import-name param-types ... -> result-type

Define scheme-name, a procedure wrapping the Wasm import import-name in the namespace namespace.

The signature of the function is specified by param-types and result-type, which are all Wasm types expressed in WAT form.

Valid parameter types are:

  • i32: 32-bit integer
  • i64: 64-bit integer
  • f32: 32-bit float
  • f64: 64-bit float
  • (ref string): a string
  • (ref extern): a non-null external value
  • (ref null extern): a possibly null external value
  • (ref eq): any Scheme value

Valid result types are:

  • none: no return value
  • i32: 32-bit integer
  • i64: 64-bit integer
  • f32: 32-bit float
  • f64: 64-bit float
  • (ref string): a string
  • (ref null string): a possibly null string
  • (ref extern): a non-null external value
  • (ref null extern): a possibly null external value
  • (ref eq): a Scheme value
Procedure: external? obj

Return #t if obj is an external reference.

Procedure: external-null? extern

Return #t if extern is null.

Procedure: external-non-null? extern

Return #t if extern is not null.

Syntax: define-external-type name predicate wrap unwrap
Syntax: define-external-type name predicate wrap unwrap print

Define a new record type named name for the purposes of wrapping external values. predicate is the name of the record type predicate. wrap is the name of the record type constructor. unwrap is the name of the record field accessor that returns the wrapped value. Optionally, print is a procedure that accepts two arguments (obj and port) and prints a textual representation of the wrapped value.


Next: Evaluation, Previous: Pattern matching, Up: Scheme reference   [Contents][Index]