using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Pipeline.CodeReload;
using IlInterpreter.Interpreter;
namespace Unity.Pipeline.Compilation
{
///
/// IL2CPP-safe in-place code reload backend: instead of Assembly.Load + reflection, the
/// transformed override assembly bytes run in the IlInterpreter interpreter, and each override is
/// registered as an interpreter dispatch in .
///
/// Override methods are static {ret} {Method}({TargetType} instance, ...), dispatched as
/// interpreter.Invoke("{Method}", instance, ...args). The host binding is the shared
/// standard surface plus the target type — public and non-public members (the override is
/// compiled with access checks disabled). Other referenced types bind on demand during Load via
/// the binding's AutoBindResolver.
///
/// Inherits the interpreter's limits: constructs IlInterpreter doesn't lower yet throw at dispatch
/// time, and falls back to the original
/// compiled body for that call. Member refs unbound at Load are reported through the
/// warnings out param so the reload response shows the gap before play-mode execution hits it.
///
static class InterpreterCodeReloadExecutor
{
// Editor and any development player (incl. IL2CPP on device): the interpreter runs pushed IL
// bytes without Assembly.Load or Roslyn.
#if UNITY_EDITOR || DEVELOPMENT_BUILD || ENABLE_RUNTIME_PIPELINE
///
/// Load into an interpreter and register an interpreter-backed
/// override for each entry on .
/// A name with no woven dispatch target (the method — or its whole type — was added since
/// the running build was compiled) cannot be dispatched from compiled code and is reported
/// through ; it still loads into the interpreter, so other
/// reloaded bodies in the same file can call it.
/// Returns the method ids that bound; carries
/// reasons for any that didn't; carries non-fatal load-time
/// diagnostics (host members that resolved to throwing stubs — the reload still applies, and
/// a stub only throws if its call site is reached).
///
public static List Register(
byte[] assemblyBytes, string typeName, IEnumerable methodNames,
out List skipped, out List warnings)
{
var registered = new List();
skipped = new List();
warnings = new List();
var names = new List(methodNames ?? Array.Empty());
if (assemblyBytes == null || names.Count == 0)
return registered;
// All methods in one in-place file share the target type, but not every NAME resolves:
// a [CodeReload] method added since the last compile has no woven prologue to dispatch
// from, so its id is not in the registry. Resolve the type from the first name that is;
// the rest load but only intra-file calls reach them (reported below). A file whose type
// was never compiled resolves nothing — targetType stays null and the binding runs
// without the target-type overlay.
Type targetType = null;
var resolvable = new List();
var unwovenNames = new List();
foreach (var m in names)
{
if (CodeReloadRegistry.TryGetReloadableDeclaringType($"{typeName}.{m}", out var t))
{
targetType = targetType ?? t;
resolvable.Add(m);
}
else
{
unwovenNames.Add(m);
}
}
IlInterpreter.Interpreter.ScriptInterpreter interpreter;
try
{
// Standard shared surface plus the target type so the rewritten body can touch its
// own members, public and non-public. Every other type the override references is
// bound on demand by the Load pass via the binding's AutoBindResolver.
var binding = IlInterpreterHostBindings.CreateStandard();
if (targetType != null)
binding = binding.AllowType(targetType)
.AllowNonPublicInstanceMembers(targetType);
interpreter = new IlInterpreter.Interpreter.ScriptInterpreter(
binding, msg => Debug.Log(msg));
// Lenient: a method that can't be lowered becomes a skipped stub (reported per method
// in the registration loop below) instead of failing the whole file's reload — so one
// bad method (e.g. SetupUI referencing a member missing on the running build) doesn't
// drop the other reloaded methods (UIUpdate) in the same file.
interpreter.Load(new OverrideScript(assemblyBytes), lenient: true);
}
catch (Exception ex)
{
// Lenient load no longer throws for per-method lowering failures, so this is a
// whole-assembly failure (parse error, cctor, a genuine crash). Anything but a
// ScriptValidationException carries an unactionable message ("Index was outside the
// bounds of the array") — name the type and put the full stack in the log.
var detail = ex is IlInterpreter.ScriptValidationException
? ex.Message
: $"{ex.GetType().Name}: {ex.Message} (full stack in the editor log)";
Debug.LogError($"CodeReload: interpreter load failed for '{typeName}': {ex}");
foreach (var m in names)
skipped.Add($"{m}: interpreter load failed: {detail}");
return registered;
}
// These member refs resolved to throwing stubs. The reload still applies (a stub only
// throws if reached), but report the gap in the reload response rather than as a mid-play
// ScriptRuntimeException on whichever frame first hits it.
if (interpreter.UnboundHostMembers.Count > 0)
warnings.Add(
$"warning: {interpreter.UnboundHostMembers.Count} host member(s) referenced by the " +
$"reloaded code are not in the binding and will throw if reached: " +
$"{string.Join(", ", interpreter.UnboundHostMembers)}. Types outside the standard " +
"surface and demand-time auto-bind are not callable from interpreted code; " +
"BCL types beyond the shims are intentionally not auto-bound.");
foreach (var methodName in resolvable)
{
var targetMethodId = $"{typeName}.{methodName}";
var name = methodName; // capture per iteration
// Skipped under lenient load — its body couldn't be lowered. Report the reason and
// move on rather than register a dispatcher that throws on first call. The most
// common cause across the editor→player boundary is a reference the running build
// lacks, so make that actionable.
if (interpreter.TryGetLoweringSkip(methodName, out var lowerSkip))
{
if (lowerSkip.Contains("did not resolve to a host or script method"))
lowerSkip += " — the referenced member is neither an interpreted " +
"(reloaded/[CodeReload]) method nor a bound host member. If it was added " +
"since the running build was made, rebuild/redeploy the player to sync, " +
"or mark it [CodeReload].";
skipped.Add($"{methodName}: interpreter load skipped this method: {lowerSkip}");
continue;
}
object Dispatch(object instance, object[] parameters)
{
// Override signature: static M(TargetType instance, ...originalArgs). Use typed
// Invoke overloads for the common arities to avoid a per-call object[] (args are
// already boxed by the woven prologue). Returns the interpreter's (boxed) result,
// which the woven prologue of a value-returning method unboxes; void methods
// return null, which that prologue ignores.
int n = parameters?.Length ?? 0;
switch (n)
{
case 0: return interpreter.Invoke