✅ Sharing executing Assembly in Roslyn script
Howdy, this could be a very specific use case, but if anyone has any insights here I would appreciate it:
I am hosting am embedded .net runtime in a c++ app using
nethost.h
and loading my assembly and getting fn pointers with load_assembly_and_get_function_pointer
. It's invoking everything fine. In the function I'm calling on the .net side I am using Roslyn to run .csx scripts. In that .net lib I define an interface INpcEvent
and in the .csx I create a class that implements that interface and returns a new instance. I want to be able to get a reference to that new instance via this code:
var scriptOptions = ScriptOptions.Default.WithReferences(Assembly.GetExecutingAssembly());
var state = await CSharpScript.RunAsync<GlobalType.INpcEvent>(text, scriptOptions, globals: global, globalsType: typeof(GlobalType));
And I get the error Exception thrown: 'System.ArgumentException' in System.Private.CoreLib.dll: 'Cannot bind to the target method because its signature is not compatible with that of the delegate type.'
- seems to be that although they're the same types it's not the same assembly reference so it can't be cast as the return value. I see that Roslyn is loading that dll in runtime with the WithReferences option. Is there a way to use what I have defined in memory as the caller for types to sync those up?7 Replies
Did some debugging and they were indeed two different assemblies in memory although pointed to the same module on disk... Maybe this could be part of the embedded runtime from
nethost
?
Just to "get things work" i used a workaround with reflection in the assembly but I'm pretty sure it's duplicating it for every instance I call the script uncached which is a no go. Anyone have any insight into sharing an assembly across Roslyn scripts or an alternative for script compiling?
var scriptOptions = ScriptOptions.Default.WithReferences(MetadataReference.CreateFromFile($"{directory}/DotNetTypes.dll"));
var state = await CSharpScript.RunAsync<object>(text, scriptOptions);
NpcMap[npcName] = state.ReturnValue;
var instance = NpcMap[npcName];
var createNpcEvent = instance.GetType()?.BaseType?.Assembly.ExportedTypes.FirstOrDefault(f => f.FullName == "EqFactory")?.GetMethod("CreateNpcEvent");
if (createNpcEvent != null)
{
var npcEvent = createNpcEvent.Invoke(instance, [initArgs?.Zone, initArgs?.EntityList, npcEventArgs.Npc, npcEventArgs.Mob, message ?? ""]);
var methodInfo = instance.GetType().GetMethod(MethodMap[id]);
if (methodInfo != null)
{
methodInfo.Invoke(instance, [npcEvent]);
}
}
so, the problem is that the default logic used by the script engine to load dependent assemblies for the script is not friendly for using dependencies from a non-
Default
AssemblyLoadContext
load_assembly_and_get_function_pointer
loads components into isolated AssemblyLoadContext instances. so, the assembly you load using it (the one which defines INpcEvent
) is not in AssemblyLoadContext.Default
you use CSharpScript.RunAsync
, which loads the generated script assembly into a fresh ALC that it creates. (so, the assembly defining INpcEvent
is not in it). when you run the script, it needs to load that assembly (since the script uses that type). because that assembly is not in the script ALC or AssemblyLoadContext.Default
, it falls back to some assembly loading logic provided by the script engine
the script engine remembers the Location
of the assembly you provided in WithReferences
and uses that to load the assembly defining INpcEvent
into the script ALC. that's why you end up with a duplicate--the assembly is loaded both into the component ALC created by the unmanaged hosting layer and into the script ALC created by Roslyn. and that's why the error says that the signature is incompatible
the easiest fix here is telling the script engine to reuse your already loaded assembly instead of re-loading it into the wrong ALC. you can do this by writing: and passing loader
to CSharpScript.Create
. then delete your workaround
@temp0@reflectronic Worked like a charm, thank you! Was trying to run through RunAsync the whole time, didn't see the
Create
method. Much appreciated! How do I mark this answer as complete?$close
Use the /close command to mark a forum thread as answered
:)
@reflectronic Wasn't sure if I should open another thread for this question but you seem to know your way around roslyn/ALCs... It seems loading up script object and running
Compile()
will load a lot of necessary assemblies and incur about 50mb overhead in memory, is there any design pattern for reclaiming memory other than running in a separate process? I have tried a custom ALC with .Unload() at the end but I think it's moreso all the libs that roslyn loads in that take up that memory