Dynamically Implementing Interfaces at Runtime

Good day all, For a modding framework refactor/rework, we are implementing interface-based events, which can be defined by mods and will be loaded at runtime as well, ie:
public interface IEventUpdate : IEvent
{
void Update(float deltaTime);
}

// Subcription is automatic for implementing types
public class SomeUpdateableModClass : IEventUpdate
{
void Update(float deltaTime) { /* do something */}
}
public interface IEventUpdate : IEvent
{
void Update(float deltaTime);
}

// Subcription is automatic for implementing types
public class SomeUpdateableModClass : IEventUpdate
{
void Update(float deltaTime) { /* do something */}
}
and an event publisher will follow a pattern as follows:
public class SomeModClass
{
private readonly EventService _eventService;
public SomeModClass(EventService eventService){ /* DI Injection */ }

void Pushupdate()
{
_eventService.PublishEvent<IEventUpdate>((subscriber) => subscriber.Update(20));
}
}
public class SomeModClass
{
private readonly EventService _eventService;
public SomeModClass(EventService eventService){ /* DI Injection */ }

void Pushupdate()
{
_eventService.PublishEvent<IEventUpdate>((subscriber) => subscriber.Update(20));
}
}
The issue we have is that we support Lua scripting via MoonSharp, which includes events. This was here before and we have the legacy API which must be supported as follows:
EventService.Add("IEventUpdate", "someIndentifierString", function(deltaTime)
-- do something
end)
EventService.Add("IEventUpdate", "someIndentifierString", function(deltaTime)
-- do something
end)
So, I've been investigating using some combination of Linq.Expression and ExpandoObject or dynamic to dynamically construct a runtime proxy type that implements the interface but searching has been kind of a pain because all major examples are for either .NET Framework and don't work in .NET Core, or they're for Mocking/Testing and are answered using Moq, Castle or some other testing-targeted framework. I'd like to avoid having to Emit an IL method dynamically. We have some of this already for dynamic hooking classes and maintain it has been a growing pain. What are my options here? Because this is my goal in pseudo code:
public delegate void LuaCsAction(params object[] args);

public class EventService : IEventService
{
// ... assume there're collections up here
public void Add(string eventId, string identifier, params LuaCsAction[] args)
{
var obj = new ExpandoObject();
var mArr = _eventTypes[eventId].GetMethods();
for(int i=0; i<mArr.Length; i++)
{
((IDictionary<string, object>)obj)[mArr[i].Name] = args[i];
}

_eventSubscribers[_eventTypes[eventId]][identifier] = obj; //yes, I'm ommitting the cast.
}
}
public delegate void LuaCsAction(params object[] args);

public class EventService : IEventService
{
// ... assume there're collections up here
public void Add(string eventId, string identifier, params LuaCsAction[] args)
{
var obj = new ExpandoObject();
var mArr = _eventTypes[eventId].GetMethods();
for(int i=0; i<mArr.Length; i++)
{
((IDictionary<string, object>)obj)[mArr[i].Name] = args[i];
}

_eventSubscribers[_eventTypes[eventId]][identifier] = obj; //yes, I'm ommitting the cast.
}
}
Anyways, any help is appreciated.
15 Replies
Anton
Anton3w ago
You shouldn't do this intrusively like that since you have that legacy. Decouple events from interfaces. The new way should just be an option of wiring the handlers. Also, I wouldn't do this automatically. I'd make this behavior opt-in so people could keep wiring their stuff manually if they want to. To me the best solution would be to make the underlying handlers you actually store and execute more generic So lua could wire to that directly But yeah the way tou have it is tough. You probably should let your system call the handlers and forward the args Another option is to source generate a wrapper class that calls a lua handler for each interface and have all mods provide that Then find it with reflection It does require having the client adjust their build
PerfidiousLeaf
PerfidiousLeafOP3w ago
The old API for manually registerring to events is still completely supported, the events interface API is additional functionality. The problem is the old API makes all events implement either Action<object[]> or Func<object, object[]> as the delegate type. so there's no static typing support nor more complex behaviour without casting and it's entire based on reading the API docs, which weren't that well maintained, or the code. My goal was to have a wrapper/container class that would contain the actual LuaMethod delegate and implement the interface, then redirect the call to either the Action or Func handler based on signature.
Anton
Anton3w ago
the best way is to source generate on each client or create a runtime wrapper, which you said you don't want to do or rethink your design
PerfidiousLeaf
PerfidiousLeafOP3w ago
Well the runtime wrapper would probably be dynamic assembly IL emission. And the problem with "wizardry" in community modding frameworks is they are impossible to maintain once the "wizard" leaves the project. and curses get inserted into the spells via PRs
Anton
Anton3w ago
e.g. having the method have the same name for each event and having it derive from a generic interface, which might not be optimal from the static typing perspective source generation is pretty easy to integrate you just include a package
PerfidiousLeaf
PerfidiousLeafOP3w ago
I have used it at work. The issue is most of the modding community uses Lua and/or their IDE of choice is either Notepad++ or VSCode :BaroDev: Oh yeah forgot to mention the best technical debt The old system until recently only supported C# in the form of source files included in the package and compiled at runtime. Anyways, ideally, everyone would be in Cs land but Cs was basically addded later. I will rethink my approach though
Anton
Anton3w ago
sounds like lots of fun
PerfidiousLeaf
PerfidiousLeafOP3w ago
Worst case is that these systems remain separate (since we use the new event system internally).
Anton
Anton3w ago
I'd love to work there
PerfidiousLeaf
PerfidiousLeafOP3w ago
lookup LuaCsForBarotrauma lel or if you mean actual job, source gen because web dev work. I'm surprised that there isn't a cleaner way to forward a delegate call to a generic object[] array sig
Anton
Anton3w ago
wdym here?
PerfidiousLeaf
PerfidiousLeafOP3w ago
asp net, source gen on backend
Anton
Anton3w ago
We might be misunderstanding each other, I just said that working on this modding integration thing in a game with lots of tech debt where you have to support legacy sounds like fun No sarcasm
PerfidiousLeaf
PerfidiousLeafOP3w ago
I mean, I am enjoying working on it. However, my primary motivation for joining the team was to roll my add-on toolkit into it lol. I've learned a lot while doing it so far.
SleepWellPupper
You mentioned wanting to avoid emitting due to maintainability concerns. Also SGs would require recompilation, which you say some users won't be able/willing to do. You could emit class implementations dynamically not using Emit but roslyn, i.e. dynamically compiling source code that you can stitch together at runtime, then loading that new assembly to get the type. This way, no "wizardry" concerning emitting would be needed, think string manipulation and some roslyn api calls. Maybe this is useful.
Want results from more Discord servers?
Add your server