Closed lukewagner closed 4 years ago
Prior art: Nim has a defer statement which is implemented in terms of try-finally.
The neat thing here is we don't need to specify a general try/catch/finally mechanism in the interface layer in order to get the right behavior here. The stack copying at the time of the defer is a good idea as well, mostly because it lets us duplicate values without needing to reimplement locals or something.
Two extra use cases to consider for any solution to this problem (which are supported by the above defer
instruction, but not with the more-regular alternatives I've imagined):
string
(or other interface values).string
(or other interface value) can be consumed by N string-to-memory
instructions.I would've assumed defer would be something in a higher level system that generates the section, not part of the section format itself. nvm i can't read. I like the idea of defer
in this case.
Resolved, at least as an initial draft, in #63.
There is a TODO at the end of the "Export returning string (dynamically allocated)" section which describes two important problems that we need to solve (not just for strings, but for any type when returned as dynamically-allocated linear memory from a wasm export).
The two issues that need to be solved are:
call-export "foo"
wherefoo
is wasm code that allocates and returns linear memory (or some other resource that needs to be released by a subsequent call), if an exception is thrown (in the future when we have exception-handling), the allocation must be released during unwindingmemory-to-string
, but rather some time after the consumingstring-to-memory
of the caller, so that the engine can perform amemcpy
from source to destination.Since this is not an esoteric problem, but one everyone will hit early and often, we'd ideally offer a simple, compact, and "canonical" solution to both of these problems.
One possible solution is to have a
defer
instruction that says "call the given export with copies of the top-of-stack values (the count and types determined by the export's signature) at the end of the adapted call". Here, "adapted call" means treating the adapter function of the caller and the adapter function of the callee as a single call, as if the callee was inline into the caller (which is the whole point, from an optimization POV).So, the current example:
could be replaced with
Noting that:
free
takes a singlei32
and thusdefer "free"
will take a copy of the single top stack value (which is validated to be ani32
), which we assume is the pointer of the (pointer,offset) pair.defer
always immediately follows thecall
; in a less-trivial example, other adapter instructions (that can throw) could be inserted between thedefer
andmemory-to-string
, so the immediatedefer
ensures"free"
is called in all cases.Spec-wise, I imagine that the configuration (the input and output of every instruction) would contain a vector of (export,arguments) pairs that is appened to by
defer
and executed (LIFO, presumably) at the end of the adapted call.This solution does feel a little "special" and irregular, though, so I'm interested to hear about any other options that are more regular. One way to rationalize this is to consider the adapted function callee to be inlined into the adapted function caller, without introducing a new scope, and then the
defer
is like a C++ RAII stack object.