wasm-c-api
wasm-c-api copied to clipboard
Should re-exported functions preserve identity?
Consider a module that imports a function and exports it again:
(module
(func (import "" "f") (export ("f"))
)
and host code that triggers this round trip:
own<Func> f1 = Func::make(...);
auto instance = Instance::make(..., {f1.get()});
auto exports = instance->exports();
Func* f2 = exports[...]->func();
which leads to the question: should f1 and f2 have the same identity?
In other words: should f1->same(f2) return true or false? In yet other words, if host data was stored on f1 using f1->set_host_info(...), should f2->get_host_info() return that host data?
I think reasonable arguments can be made both ways, and I think it mostly boils down to what one assumes about knowledge of host and module about each other.
(A) If one assumes that the host knows what the module does, then intuitively it makes sense that round-tripping preserves identity: if I pass in a thing and get the same thing handed back out to me, then of course it should be the exact same thing!
(B) If one assumes that the module is a "black box" from the host's point of view, then it makes sense for the host to assume that each export is a unique object, and assumptions about shared identities are inherently unsafe. After all, the module could be doing (or in a new version be changed to doing) this instead:
(func $f (import "" "f"))
(func (export "f") (call logging) (call $f))
which would provide the same interface to the host, but break the assumption about identity.
Implementation-wise, both possible behaviors are doable. V8's current implementation just so happens to do case (B) above, but that should not be a deciding factor here.
My own opinion at this time is that I'm leaning towards (B), because I think the "black box" assumption is more likely in actual deployment scenarios (the decoupling of module and host is kind of the point of running Wasm modules in the first place), and the white-box scenario where (A) would make more sense probably mostly shows up in small artificial testcases. That said, I could be convinced otherwise.
Either way, I think it would be good to specify the expected behavior, or alternatively at least document officially that it is unspecified :-)
(Unrelated side note: I still find it a surprising footgun that instance->exports()[0]->func() leads to a crash; but I'm not sure what to do about it.)
Good question. For the JS API, (A) holds. But it might be less clear that it should be enforced on the C/C++ level as well, since that might come with extra costs that are undesirable there. Can you think of any such costs, perhaps in other engines? If so, it should perhaps be unspecified.
I don't think we want to specify (B), since that seems neither useful behaviour nor necessarily the cheapest option for all engine implementation strategies (some might have to allocate redundant clones just to satisfy it).