Are C# expression trees supported on all PC, Mac and Mobile target platforms?

Hi, I just learnt about C# expression trees, never used them before. This week I implemented a trigger system based on reading file data and compiling them into runtime delegates and actions.

I think I am just using basic functionality:

var contextParameter = Expression.Parameter(typeof(TContext), "context");
var castedContext = Expression.Convert(contextParameter, contextType);
var parameterExpressions = new Expression[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
    var delegateInstance = Expression.Constant(parameters[i]);
    var delegateEvaluateMethod = parameters[i].GetType().GetMethod("Evaluate");
    var evaluateCall = Expression.Call(delegateInstance, delegateEvaluateMethod, castedContext);
    parameterExpressions[i] = evaluateCall;
}

var methodCall = Expression.Call(castedContext, methodInfo, parameterExpressions);
var lambda = Expression.Lambda<Func<TContext, TResult>>(methodCall, contextParameter);

return lambda.Compile();

After fully implementing the system, it suddenly occurred to me that I should check for platform support, but the earliest discussions I found are from 2017/2018.

From what I understand, I am doing a JIT compile and this is only supported on Mono scripting backend. For mobile platforms, only IL2CPP scripting backend is supported so that means what I am doing here won’t work on mobile.

Is my understanding correct, and is there anyway I can workaround the limitation without tearing down everything I have done?

So did I. Just now.

Or maybe I’ve come across this for a split second in context of Roslyn and runtime compilation.

Most likely correct, since iOS definitely and Android possibly maybe doesn’t allow runtime code modification or compilation UNLESS the app absolutely requires it (ie a “learn to code” app).

Yes, and look at that code: not at all basic.

If you want runtime reload or script execution, by all means use a scripting language. Lua-CSharp I can recommend. Especially if it comes to serialization - Lua tables are easy to write, and reading is simply “dostring(fileContents)” and you get a usable collection back.

The last thing you want to do is read data that compiles into code in order to represent data.

If you strictly separate code from data, what you get is trivial serialization and runtime data reload, while your code will be cleaner and easier to debug, too.

So basically what you did is to build a rocket that can fly into space, except you really only want to go to the mall. In other words: it smells like the wrong tool for the job, and the overengineering potential is immense. :wink:

afaik nothing from the Reflection.Emit namespace is usable in IL2CPP. Dont know about any workarounds other than re-designing what you are trying to achieve

Thanks for the confirmation on the issues with expression trees. :joy:

Sigh… luckily the rocket can be reduced to only a single method, albeit a bit bulky, that creates a Func<> delegate. I have replaced the internals with Delegate.CreateDelegate()

var funcType = data.Parameters.Length switch
{
    1 => typeof(Func<>).MakeGenericType(typeArray),
    2 => typeof(Func<,>).MakeGenericType(typeArray),
    3 => typeof(Func<,,>).MakeGenericType(typeArray),
    4 => typeof(Func<,,,>).MakeGenericType(typeArray),
    _ => throw new NotSupportedException()
};

return Delegate.CreateDelegate(funcType, methodInfo);

I am not familiar with Lua, it will be yet another thing to learn. For now, my trigger system is a matter of assembling events, conditions, actions and delegates – scripts maybe next time.

While this doesn’t answer your question directly, you can always test it by building your project with IL2CPP scripting backend on Windows or macOS and running it. You don’t need to deploy to a device to do that. If it works there, chances are the same scripting features will work on other platforms too (one exception being WebGL which is a bit more restrictive than most).

What’s the use case for this setup?

I get the feeling you may be trying to solve a problem the hard way because you’re not seeing (or not aware of) the obvious solution.

It kinda looks like you want to serialize callbacks (delegates). Which you could simply provide in your system, ie data says “raise event XYZ” and the code processing things simply invokes a preset event.

As to generic and multiple params that makes me suspicious. You can typically wrap multiple parameters into a single DTO (data transfer object), which you could make generic as needed.

What on earth are you doing that needs this kinda whackiness?!!!

All I see above is some kind of dialog system that perhaps sets some values at the end of it to trigger other things to happen in game. Dialog systems are never going to require wild whacky stuff like the above code, although I suppose they could be done that way.

Dialog system fundamentals and structure:

There are also some free packages you could start from:

Fungus: https://fungusgames.com

Inkle: https://www.inklestudios.com

A free Ink-Fungus gateway product:

Even if you don’t end up using either of those, you could review them for structure because it is almost certain that they have already solved all the same problems you are trying to solve.

Yes you are very perceptive and correct.

I currently have a context object which is like an event arg that is raised when some game event happens. And the context object defines the methods what are available to the event trigger system. I am serializing these methods and deserializing them.

[GameContext]
public class CEncounterStartedContext : CGameContext
{
        public CEncounterStartedContext(ISceneControl control) { ... }

        [TriggerCondition("Compare whether hero {0} {2} {1}")
        public bool HeroComparison(Hero hero1, Hero hero2, ComparisionType compareType){ ... }

        [TriggerAction("Move camera to {0}, with zoom {1} and rotation {2}")
        public void MoveCamera(Vector3 position, float zoom, float rotation) { ... }

        [TriggerDelegate("Get player heroes that are {0}")
        public Hero[] GetPlayerHeroes(HeroStatusType statusType) { ... }

        [TriggerDelegate("Get position of hero {0}")]
        public Vector3 GetPositionOfHero(Hero hero) { ... }
}

Originally, Expression Trees provide type safety up front without funky generics. The deserialization of triggers happens once at start up, and then it runs fast in a tight update loop. But Delegate.CreateDelegate() can’t do that at runtime, so this is what I end up.

I could make all methods take a single parameter DTO object and I will not need to deal with generics with unknown number of params. But that just means I complicate other parts of the system. e.g. I lose readability at compile time and the ability to apply the method description directly on method parameters which comes for free in this setup. You gain some, you lose some.

What do you think?

Heya, thanks, but no, this is not about making a dialogue player, which is just a while loop setting a text box with text.

I’d say you should look into how a Statemachine works, and how to create one. :wink:

Just based on the trigger conditions and delegates, I see the “Conditions and Actions” of a Statemachine transition. Here’s a version I made.

It would likely look like the code in my blog post, and it would be serializable because the references aren’t hardcoded but runtime lookup. Eg Hero becomes either a string heroName or int heroID. The other types are primitives that serialize easily. The methods themselves would be either conditions or actions in the statemachine.

Such an ID administration system or “registry” is highly recommended for many games, it simplifies working with references because there aren’t any floating around - they are obtained and discarded, ideally never “held on to” by subsystems because they don’t own that reference. And with IDs or names you also have clearly identifiable “soft references” that ease debugging because rather than “null” you can still report “Hero ‘Dumbledude’ reference unexpectedly missing”. This is invaluable and one of the strengths of data-driven designs.

In case you need an action to “query something” so that follow-up actions act upon it (ie all heroes which are in range) you can use nested actions, or a transition context object which gets filled by a preceding query action - basically the context object only needs a List and successive actions that perform something like “Damage” would use that list, get any components as necessary (IDamageable) and do their thing where applicable.

I might be wrong but that system definitely has all the bells and whistles I associate with Statemachines, or as an extension of that: Behavior Trees.

Thanks for sharing!

In my implementation they are layered concepts:

  1. Event system - triggers once when condition is met, a sequence of actions run once.
  2. State machine - state is updated when event changes.
  3. Behavior tree - while in a state, entity keeps polling the tree for the current behavior.

Sometimes state machine and behavior trees are intertwined, like a node in a state machine is the root of the tree, and a node in the tree is a state in the machine. But the event system is like an interrupt into that system.

The part I am focusing on right now is deserialization of actions/conditions/delegates in the trigger system. It’s actually not that complicated. Once the reflection was set up, adding new serializable delegate is literally just writing a new class method with attributes. I get automatic context filtering, names and description for “free”.

Agree with using primitives for references too. That’s what I am doing in the data layer, however, during bootstrapping, I convert them to runtime object reference once, hence the context method I showed all use object references.

On hindsight, definitely there are areas I could have done better. It’s always horrible code when we look back 1 year later.

The problem with this forum is people post a question with a few line of codes and some other people start to assume things and want to improve things they don’t even know the full context about.

It looks like you found a working solution, so there is no point to rewrite your whole system, just because some random commenters would have designed it different. You can try a different approach for your next project (if you want to), but if you change your working system now, then you will maybe get burnt out and not finish the project at all, so it’s better to continue with the “horrible code” that is working.

These days code emittance is a bit of an outdated tool. It still has some great uses but the most common on I’ve ever found for games is to generate code at runtime that would otherwise require reflection. With generators being a thing now though, like I said, emittance is a bit outdated.

It’s also not necessarily supported on all platforms. Likely you’ll find anything that enforces AoT to not allow it since, by definition, it can’t be compiled ahead of time. IOS and likely Android in the near future (if not already, I dunno. I don’t keep up with mobile anymore) don’t like anything that isn’t AoT. On desktop, Windows is likely to work fine for the near future. I can’t speak for MacOS or Linux but I imagine Linux would work fine and probably allows users to configure to their preference of support for JIT vs AoT. As for MacOS, can’t say there either but I’d be slightly surprised if it did work.

Overall, it’s a really cool tool that’s been partially superseded with newer technology and it seems much of the industry is shying away from JIT these days due to security concerns. Code emittance is one step beyond JIT so is even more precarious.

For sure, it’s a balance between getting it done and getting it perfect.

But it’s always good that people are sharing their work/advice, even if not useful now, it may become useful in the future for myself or others.

Thanks!