Bindings to Glk
Wasm2Glulx provides a complete set of bindings to the Glk 0.7.5
API. To access a Glk
function, your module should declare a function import with a module name of
"glk"
and a function name which matches the one defined in the Glk spec, but
with the glk_
prefix removed.
All parameters to Glk functions are of type i32
, and the result type is either
i32
or empty. Wasm2Glulx will typecheck your imports and give a compile-time
error if they are incorrectly declared. Wherever a Glk function takes a pointer
argument, the argument passed to the corresponding WASM function should be an
index into the module's memory. An index of zero is interpreted as a null
pointer.
Pointer arguments must not alias each other; treat all pointers as though they
were declared restrict
in Glk's C header. This is not something you have to
actually worry about, because the only Glk function which could possibly take
aliased pointers without UB according to C's aliasing rules is
glk_image_get_info
, and since those are pointers to output parameters it would
be nonsense for them to alias.
WASM Glk functions take their arguments in the same order as described in the Glk spec. This is despite WASM and Glulx having opposite calling conventions (in Glulx, the first function argument is on top of the stack; in WASM, the last argument is on top). The necessary swapping happens behind the scenes.
When an argument to a Glk function is a null-terminated string, Glulx expects
the string to be prefixed with 0xE0
(for Latin-1 strings) or 0xE2000000
(for
Unicode strings). Wasm2Glulx does not require this prefix to be included.
The generated bindings will automatically patch the prefix into memory and then
replace the original memory before returning. This works cleanly because none of
these functions take any other pointer arguments, so there's no need to worry
that the prefix patch will overwrite another argument.
There is no binding for glk_set_interrupt_handler
. However, if your module
exports a function named glulx_interrupt_handler
, it will be configured as
your interrupt handler at startup. (This was cleaner than trying to pass
function pointers in and out of WASM. Although Wasm2Glulx fully supports
funcref
, LLVM's support for it seems to be very buggy and my first experiment
with it segfaulted Clang.)
There are no bindings for Glulx's setiosys
and getiosys
instructions. Glk is
automatically set as the IO system at startup, and it cannot be changed. Your
own code is still responsible for the rest of Glk initialization, such as
creating a root window.
The Glk area
Certain Glk functions pass it ownership of memory buffers that you provide to it. These include:
glk_request_line_event
glk_request_line_event_uni
glk_stream_open_memory
glk_stream_open_memory_uni
Working with these functions is a bit more complicated. Wasm2Glulx creates a
special region of your program image, called the Glk area, which lives outside
the address space of your module's memory. The size of this region is fixed at
compile time but controllable by the --glk-area-size
command line argument.
When you call one of the above four functions, the buf
argument is an index
into the Glk area, rather than an index into memory. Unlike pointers to main
memory, 0
is an ordinary and valid Glk area offset and will not be interpreted
as a null pointer.
This extra bit of ceremony is necessary for two reasons. The first reason arises from the fact that WebAssembly is a little-endian architecture, but Glulx is big-endian and Glk expects to see big-endian data. When passing borrowed pointers to Glk, this difference is kept transparent: the generated bindings automatically swap memory into big-endian before calling Glk, and swap it back before returning. But for owned buffers, this doesn't work, because the Glk API makes it too complex for Wasm2Glulx to infer when buffer ownership has been returned to the caller and the buffer should be swapped back to little-endian. Having a separate Glk area solves this problem: the Glk area is always big-endian, memory is always little-endian, and swapping happens whenever you transfer data from one to the other. The second reason pertains to future-proofing. Currently, conversion between Glulx memory addresses and indexes into WASM memory is just a matter of adding or subtracting a fixed offset determined at compile time. However, this may change, because supporting future WASM features may make it necessary for WASM memory to move around in Glulx's address space. If some of that memory were potentially owned by Glk, then this movement would wreak havoc. Keeping the Glk area separate solves this too, by ensuring that it can always remain at a fixed address even when main memory moves around.
The following intrinsics are provided for moving data in and out of the Glk area:
(import "glulx" "glkarea_get_byte" (func (param $glkaddr i32) (result i32)))
(import "glulx" "glkarea_get_word" (func (param $glkaddr i32) (result i32)))
(import "glulx" "glkarea_put_byte" (func (param $glkaddr i32) (param $byte i32)))
(import "glulx" "glkarea_put_word" (func (param $glkaddr i32) (param $word i32)))
(import "glulx" "glkarea_get_bytes"
(func (param $addr) (param $glkaddr i32) (param $n i32)))
(import "glulx" "glkarea_get_words"
(func (param $addr) (param $glkaddr i32) (param $n i32)))
(import "glulx" "glkarea_put_bytes"
(func (param $glkaddr) (param $addr i32) (param $n i32)))
(import "glulx" "glkarea_put_words"
(func (param $glkaddr) (param $addr i32) (param $n i32)))
The first four functions read or write an individual byte or word to or from the
Glk area at offset $glkaddr
, while the second four move $n
bytes or words
between the Glk area at offset $glkaddr
and main memory at offset $addr
.
Note that the destination argument always comes first. The word functions will
perform endianness swaps as required, while the byte functions will not swap
anything.