#nullable enable using System; using System.Reflection; namespace IlInterpreter.Interpreter { partial class ScriptInterpreter { /// /// The object behind a delegate whose target is an INTERPRETED method (a method group over a /// script method, or a lambda's display-class method). The host holds an ordinary CLR delegate /// of the exact requested type — System.Action, EventCallback<T>, a custom /// delegate — and each invocation re-enters the VM, like does /// for iterators. /// /// No Reflection.Emit: the delegate is built with Delegate.CreateDelegate over a /// shape-matched generic forwarder (V0..V4 for void, F0..F4 otherwise) closed /// over the delegate's Invoke signature. Same AOT posture as HostBinding's typed fast paths; /// on IL2CPP a value-typed signature relies on full generic sharing. /// /// Lifetime: valid until the interpreter is re-Loaded or disposed — the same contract /// as every other object that crosses the boundary. /// internal sealed class ScriptDelegateAdapter { readonly ScriptInterpreter _owner; readonly ParsedMethod _target; readonly object? _receiver; // null for static targets; prepended as arg 0 otherwise readonly object _generation; // the ParsedAssembly this target was lowered with ScriptDelegateAdapter(ScriptInterpreter owner, ParsedMethod target, object? receiver) { _owner = owner; _target = target; _receiver = receiver; _generation = owner._parsed!; } public static Delegate Create( ScriptInterpreter owner, Type delegateType, ParsedMethod target, object? receiver) { var invoke = delegateType.GetMethod("Invoke") ?? throw new ScriptRuntimeException($"'{delegateType}' has no Invoke — not a delegate type"); var ps = invoke.GetParameters(); bool isVoid = invoke.ReturnType == typeof(void); if (ps.Length > MaxArity) throw new ScriptRuntimeException( $"Delegate '{delegateType.Name}' has {ps.Length} parameters — method groups over " + $"interpreted methods support up to {MaxArity}"); foreach (var p in ps) if (p.ParameterType.IsByRef) throw new ScriptRuntimeException( $"Delegate '{delegateType.Name}' has a ref/out parameter — not supported " + "for method groups over interpreted methods"); var adapter = new ScriptDelegateAdapter(owner, target, receiver); var shape = typeof(ScriptDelegateAdapter).GetMethod( (isVoid ? "V" : "F") + ps.Length, BindingFlags.Public | BindingFlags.Instance)!; int typeArgCount = ps.Length + (isVoid ? 0 : 1); if (typeArgCount > 0) { var typeArgs = new Type[typeArgCount]; for (int i = 0; i < ps.Length; i++) typeArgs[i] = ps[i].ParameterType; if (!isVoid) typeArgs[ps.Length] = invoke.ReturnType; shape = shape.MakeGenericMethod(typeArgs); } try { return Delegate.CreateDelegate(delegateType, adapter, shape); } catch (ArgumentException ex) { throw new ScriptRuntimeException( $"Could not bind '{target.Name}' to delegate type '{delegateType.Name}': {ex.Message}"); } } const int MaxArity = 4; object? Call(object?[] args) { if (_receiver == null) return _owner.InvokeDelegateTarget(_target, args, _generation); var withReceiver = new object?[args.Length + 1]; withReceiver[0] = _receiver; Array.Copy(args, 0, withReceiver, 1, args.Length); return _owner.InvokeDelegateTarget(_target, withReceiver, _generation); } static readonly object?[] s_NoArgs = Array.Empty(); // Shape-matched forwarders — one per (void?, arity). Public so CreateDelegate binds them. public void V0() => Call(s_NoArgs); public void V1(A a) => Call(new object?[] { a }); public void V2(A a, B b) => Call(new object?[] { a, b }); public void V3(A a, B b, C c) => Call(new object?[] { a, b, c }); public void V4(A a, B b, C c, D d) => Call(new object?[] { a, b, c, d }); public R F0() => (R)Call(s_NoArgs)!; public R F1(A a) => (R)Call(new object?[] { a })!; public R F2(A a, B b) => (R)Call(new object?[] { a, b })!; public R F3(A a, B b, C c) => (R)Call(new object?[] { a, b, c })!; public R F4(A a, B b, C c, D d) => (R)Call(new object?[] { a, b, c, d })!; } } }