-
Notifications
You must be signed in to change notification settings - Fork 56
Use code generation with an DynamicMethod (System.Reflection.Emit) #160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use code generation with an DynamicMethod (System.Reflection.Emit) #160
Conversation
…stead of using reflection to call a registered callback. This can improve performance and reduces heap allocations (contributing to bytecodealliance#113). This also solves issues bytecodealliance#158 and bytecodealliance#159.
The exception message now occurs from the cast '(T?)value.ExternRefObject' in GenericValueBoxConverter<T>.Unbox().
…rt compilation of dynamic code. TODO: Run the FunctionTests and ExternRefTests separately for this scenario.
|
I'll test this out in Unity tomorrow and will report back if there are any issues there. Thanks for keeping Unity in mind when developing this :) |
|
@kpreisser Unfortunately I encountered several issues in Unity :(
|
|
Hi @martindevans, Ok, I think I will revert the change to The other necessary change with the non-generic Out of interest (as it's been a while since I tried out Unity), what configuration do you use in Unity (e.g. which scripting backend)? Which .NET version does Unity currently support? Thanks! |
|
I just did an experiment with a slightly different way to do this (not using IL generation). Instead of using Since it looks like there might be more work done on this would it be possible to split out your bugfix changes (for #158 and #159) to a separate PR, that way Peter Huene can review those while we iterate on the rest?
Basically all of them - I'm developing an asset for the asset store (making WASM easy to use in Unity for safer modding and easier non-C# dependencies) so of course I want to support as wide an array of usecases as possible (currently targetting Unity 2021 as the minimum version, but I might bump that up to 2022 if necessary). At the moment Unity roughly corresponds to
That's a good point. I just did a very quick test and the project does seems to build just fine with |
Thanks! Yes, that's a good idea, I also just thought about this. That way we would have different generic overloads of void DefineFunction(string module, string name, Action callback);
void DefineFunction<T>(string module, string name, Action<T> callback);
void DefineFunction<T1, T2>(string module, string name, Action<T1, T2> callback);
void DefineFunction<T1, T2, T3>(string module, string name, Action<T1, T2, T3> callback);
void DefineFunction<TResult>(string module, string name, Func<TResult> callback);
void DefineFunction<T, TResult>(string module, string name, Func<T, TResult> callback);
void DefineFunction<T1, T2, TResult>(string module, string name, Func<T1, T2, TResult> callback);
void DefineFunction<T1, T2, T3, TResult>(string module, string name, Func<T1, T2, T3, TResult> callback);
void DefineFunction<TResult1, TResult2>(string module, string name, Func<ValueTuple<TResult1, TResult2>> callback);
void DefineFunction<T, TResult1, TResult2>(string module, string name, Func<T, ValueTuple<TResult1, TResult2>> callback);
void DefineFunction<T1, T2, TResult1, TResult2>(string module, string name, Func<T1, T2, ValueTuple<TResult1, TResult2>> callback);
void DefineFunction<T1, T2, T3, TResult1, TResult2>(string module, string name, Func<T1, T2, T3, ValueTuple<TResult1, TResult2>> callback);
// etc, for example we could support combinations for up to 16 parameters and up to 4 result valuesThis would also have optimal performance and avoid the allocations, without the need to dynamically generate code. However, this would probably need a way to auto-generate code for these methods functions (e.g. in a partial class file) when compiling For other delegate types that don't fit in this pattern, we could then still fall back to reflection if needed. |
|
I fiddled a bit with T4 text templates to generate overloads of The generated overload for the above example would look like this: public void DefineFunction<T1, T2, T3, TResult1, TResult2, TResult3>(string module, string name, Func<Caller, T1, T2, T3, ValueTuple<TResult1, TResult2, TResult3>> callback)
{
// ...
var parameterKinds = new List<ValueKind>();
var resultKinds = new List<ValueKind>();
using var funcType = Function.GetFunctionType(callback.GetType(), parameterKinds, resultKinds, out var hasCaller);
// ...
var convT1 = ValueBox.Converter<T1>();
var convT2 = ValueBox.Converter<T2>();
var convT3 = ValueBox.Converter<T3>();
var convTResult1 = ValueBox.Converter<TResult1>();
var convTResult2 = ValueBox.Converter<TResult2>();
var convTResult3 = ValueBox.Converter<TResult3>();
unsafe
{
Function.Native.WasmtimeFuncCallback func = (env, callerPtr, args, nargs, results, nresults) =>
{
using var caller = new Caller(callerPtr);
try
{
var result = callback(
caller,
convT1.Unbox(caller, args[0].ToValueBox()),
convT2.Unbox(caller, args[1].ToValueBox()),
convT3.Unbox(caller, args[2].ToValueBox()));
results[0] = Value.FromValueBox(convTResult1.Box(result.Item1));
results[1] = Value.FromValueBox(convTResult2.Box(result.Item2));
results[2] = Value.FromValueBox(convTResult3.Box(result.Item3));
return IntPtr.Zero;
}
catch (Exception ex)
{
var bytes = Encoding.UTF8.GetBytes(ex.Message);
fixed (byte* ptr = bytes)
{
return Function.Native.wasmtime_trap_new(ptr, (UIntPtr)bytes.Length);
}
}
};
// ...
}
}This would allow efficiently invoking the callback as long as one of the generic overloads is called (i.e. the delegate is known at compile-time to be |
|
Closing in favor of #163. |

Hi, this is a PR to use
DynamicMethod(System.Reflection.Emit) for generating code to call callbacks defined withFunction.FromCallback()andLinker.DefineFunction(), instead of using reflection. This improves performance (although not as much as I imagined), and avoids a number of heap allocations (e.g. allocating the arguments array, and boxing the arguments and return values), thereby contributing to #113.Additionally, I fixed the handling of nested ValueTuples, which previously only worked one level. For example, returning a
ValueTuple<..., ValueTuple<..., ValueTuple<int>>>(= 15 values) now should work correctly. Additionally, issues #158 and #159 should be fixed with this change.I also changed the static
Function.FinalizerandValue.Finalizerdelegates to a method with anUnmanagedCallersOnlyAttributeand use it as function pointer (delegate* unmanaged<IntPtr, void>), since it is a static method, which can also improve performance.Edit: I updated the PR so that when the current .NET runtime/platform interprets or doesn't support dynamic code (e.g. on Unity when using the IL2CPP backend, where it might be possible to use precompiled wasm modules that don't require a JIT), reflection is still used as a fallback to call the callback.
For example, when using a
Func<Caller, int, long, string, ValueTuple<int, float, double, long, object, Function, int, ValueTuple<int>>>, the generated code will be equivalent to the following:The most performance boost occurs when defining a function with a single parameter. When testing with the following code with .NET 7.0.0-rc.1 on Windows 10 Version 21H2 x64, using an
Action<int>:Before the change, the times are listed as follows (when compiling for
Release):After the change:
However, when using more than one arguments, the time with reflection suddenly decreases, and the performance gain is much less. For example, using a
Action<int, float, long>:Before the change:
After the change:
Testing with a
Func<int, float, long, ValueTuple<int, int, long>>:Before:
After:
Note: Currently the delegate types that can be used to define a callback are limited to
Action<...>andFunc<...>. I think this restriction could be lifted in the future, to allow delegates of any type. (This would require a change inFunction.GetFunctionType(), to not iterate through the generic type arguments, but interate through the parameter types of the delegate'sInvokemethod.)Another approach could be to use a source generator, as noted in the previous comments in
Function.InvokeCallback(). I chose theDynamicMethodapproach as it is agnositc to the consumer's language used (e.g. C#, F#, VB.NET etc), and also works if the assembly that defines the delegates has already been compiled (e.g. in a plugin system).A difference is that with the
DynamicMethodapproach, code (IL, and then native code by the JIT when called) is generated when defining the methods; however I think that is negligible since because this usually happens when there is already another code generation (byWasmtimeitself).However, the .NET runtime/platform must support compiling dynamic code. If dynamic code isn't supported, or is supported but would be interpreted (which would probably cause a performance slowdown), we need to fall back using reflection to call the callback.
What do you think?
Thanks!