allow parent functions to be passed to nested instances? #12
Description
Currently, the explainer disallows nested instances from importing any of the parent module's function, memory, table or global definitions. E.g., you can't write:
(module $M
(func $foo ...)
(instance $j (instantiate ... (func $func))))
The reason is that $foo
closes over $M
's instance which is created after the $j
instance. With memories, tables and non-immutable-ref.func
-initialized globals, there is no such problem, so we could perhaps allow them (which feels a bit irregular, which is why I didn't put this in the initial explainer writeup... but we could).
There is a coherent way that could allow functions too, though, which would make the instantiation rules nicely uniform. But I'm not convinced it's a good idea; I mostly just wanted to post the idea for discussion and future reference.
The observation is that there's only a problem in $M
if $j
calls $foo
during $j's start function. But calling imports from start functions is generally a bad idea; start functions should only do instance-internal stuff like setting up memory/tables/etc. So what if $foo
was initially created in a state with no instance, such that calling $foo
traps, and once $M
's instance is created, $foo
is updated to be a proper callable funcinst
. Then $foo
could be passed to $j
as above, and as long as $j
didn't call $foo
during its start function, everything would be peachy.
Incidentally, functions are already set up in the spec to be "stateful" in this manner, since a funcaddr
is the address of a funcinst
in the store, which means that the funcinst
s can technically be mutated just like memories/tables/globals (not that we should, but we could ;).
Performance/complexity is obviously a concern, but I think this could be pretty simple and cheap. Basically, for the small subset of functions locally statically observed to be passed into instantiate
, their prologue would start with a cheap branch.
One use case would be if you have a parent module P
that wants to reuse a child module C
that imports callback functions as function imports (say, C
is a hash table and the callback is the hash function) and P
wants to supply its own callbacks. Without the above relaxation, P
will have to resort to gross indirections like those in the dynamic linking cyclic dependencies example which will be significantly slower than the "branch in prologue" implementation mentioned above. But I'm not sure how compelling this use cases is?
Incidentally, with the Interface Types rebase onto Module Linking, there is a very strong use case since this situation arises with every import adapter. In particular, import adapters both need to be imported by the nested core wasm module (so they can adapt its imports) and be able to call back into core wasm (for malloc
). But this relaxation could be kept to the Interface Types layer and out of core wasm, so this isn't necessarily a use case either.
Thoughts?