For the last couple of days I've been contemplating how to improve the Caller system, what I've come up with entails a large refactor of how things are done so I thought I'd lay out my thoughts and get some feedback. @peterhuene and @kpreisser I'd really value your input :)
Context
At the moment a callback from WASM into C# can optionally take a Caller parameter which gives the call some context (e.g. access to Memory etc). For example:
Linker.DefineFunction("env", "check_string", (Caller caller, int address, int length) =>
{
caller.GetMemory("mem").ReadString(address, length).Should().Be("Hello World");
});
The Problem
At the moment this mechanism always allocates a Caller for every call and will also often be used to fetch a Memory or Function which is in turn allocated. That's not great for performance (particularly in Unity, which is where I'm using wasmtime).
The Solution?
The obvious solution to this is to make Caller a ref struct. This also better models the intended lifetime of the Caller as well - you're not meant to hold it beyond that one method call.
However there is a problem with this - Caller implements IStore and in turns passes itself into Memory and Function constructors as the store reference. A ref struct cannot implement an interface so this doesn't work (it also can't be passed into a generic method, so we can't refactor to something like Invoke<T> where T : IStore either). So if we want to get a Memory or Function we'd be back needing two allocations, one for the store and one for the object itself.
To solve both problems we could introduce new memory/function types which are ref structs (e.g. RefStructMemory) which are returned by new methods (e.g. Caller.GetRefMemory). Since these would be ref structs they could contain the StoreContext directly. Again this also models the intended lifetime better.
Obviously this is a lot of duplicated code, but I think we could reduce that by putting all of the actual work inside the RefStructXXX types and the current class types would become wrappers which internally create the relevant RefStructXXX, call into it and immediately discard it.
Thoughts?
I realise this is a huge amount of churn, but given that it improves the handling of lifetimes as well as performance I think it's probably worth it.
For the last couple of days I've been contemplating how to improve the
Callersystem, what I've come up with entails a large refactor of how things are done so I thought I'd lay out my thoughts and get some feedback. @peterhuene and @kpreisser I'd really value your input :)Context
At the moment a callback from WASM into C# can optionally take a
Callerparameter which gives the call some context (e.g. access toMemoryetc). For example:The Problem
At the moment this mechanism always allocates a
Callerfor every call and will also often be used to fetch aMemoryorFunctionwhich is in turn allocated. That's not great for performance (particularly in Unity, which is where I'm using wasmtime).The Solution?
The obvious solution to this is to make
Calleraref struct. This also better models the intended lifetime of theCalleras well - you're not meant to hold it beyond that one method call.However there is a problem with this -
CallerimplementsIStoreand in turns passes itself intoMemoryandFunctionconstructors as the store reference. Aref structcannot implement an interface so this doesn't work (it also can't be passed into a generic method, so we can't refactor to something likeInvoke<T> where T : IStoreeither). So if we want to get aMemoryorFunctionwe'd be back needing two allocations, one for the store and one for the object itself.To solve both problems we could introduce new memory/function types which are
ref structs (e.g.RefStructMemory) which are returned by new methods (e.g.Caller.GetRefMemory). Since these would be ref structs they could contain theStoreContextdirectly. Again this also models the intended lifetime better.Obviously this is a lot of duplicated code, but I think we could reduce that by putting all of the actual work inside the
RefStructXXXtypes and the current class types would become wrappers which internally create the relevantRefStructXXX, call into it and immediately discard it.Thoughts?
I realise this is a huge amount of churn, but given that it improves the handling of lifetimes as well as performance I think it's probably worth it.