Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## How a Reload Applies

1. Resolves each file to its compiled assembly via `CompilationPipeline`, then groups the files by that assembly. Each group runs the worker once and produces one shim assembly, so the members one file adds are visible to the bodies edited in its siblings; groups are processed one after another and never affect each other.
2. Rewrites each editable method body of the group into a static shim in an out-of-process Roslyn worker. When an async, iterator, lambda, local-function, or LINQ-query body touches private/internal members, those accesses are rewritten to accessor delegates so the body can compile and run from the shim assembly (the delegation shape in step 4).
2. Rewrites each editable method body of the group into a static shim in an out-of-process Roslyn worker. When an async, iterator, lambda, local-function, or LINQ-query body touches private/internal members, those accesses are rewritten to accessor delegates so the body can compile and run from the shim assembly (the delegation shape in step 4). An `internal` member of a type the reload was not given cannot be rewritten this way, so such a body is `Skipped` (`scope-and-limits.md`).
3. Compiles the group's shims into one assembly against publicized reference copies, loads the result into the Editor domain, and binds every shim type's accessor delegates (`__BindAccessors`) before any patch is applied.
4. Patches each original method with a Harmony transpiler (ID `io.github.hatayama.uloop.hot-reload`) in one of two shapes: transplant copies the shim's IL into the original method, while delegation rewrites the original to forward its arguments to the shim, which runs as normally compiled code.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,8 @@ source on disk. When a run skips a method it had patched before, `Warnings` name
| Method on a `partial` type when another part of the type changed since the last compile and was not passed, or when a file that names the type has syntax errors (passed or not) | Hot reload binds against the compiled type; pass that file with `--files` too, or run `uloop compile`. For a file with syntax errors, fix it and run hot reload again |
| Method on a `partial` type when the other parts could not be checked against the last compile (no source snapshot yet, or more than 50 changed files in the assembly) | Run `uloop compile` |
| Method on a `partial` type whose body names a member no source file of the assembly declares | A part generated at compile time (a source generator's output) is not visible to hot reload; run `uloop compile` |
| Method or getter on a `partial` type whose body uses an `internal` member of a type the reload was not given, by its bare name, as a method passed as a delegate, inside a lambda, local function, query, iterator or async method, in a body where a lambda, local function or query works with the member's result, or in a body patched through a delegating shim | Hot reload reaches such a member only as a field, a property or a method call in the method's own statements, written with its receiver (`this.Name`, `Type.Name`, `value.Name`); qualify a bare name, or run `uloop compile` |
| Method or getter whose body uses an `internal` member of a type the reload was not given, by its bare name, inside a lambda, local function, query, iterator or async method, in a body where a lambda, local function or query works with a value hot reload could not resolve (such as the member's result), or in a body patched through a delegating shim | Hot reload reaches such a member only in the method's own statements, written with its receiver (`this.Name`, `Type.Name`, `value.Name`); qualify a bare name, or run `uloop compile` |
| Method or getter on a `partial` type whose body passes an `internal` method of such a type as a delegate, uses an `internal` event of it, or names an `internal` member of it in an object initializer or a property pattern | On a `partial` type hot reload patches such a member only as a field, a property or a method call written with its receiver; run `uloop compile` |
| Method or getter on a `partial` type whose body uses an `internal` member of a type in another assembly (through `InternalsVisibleTo`) | Reported as a name no source file of the `partial` type declares; hot reload does not patch this use from a `partial` type yet. Run `uloop compile` |
| Method on a struct (value type) | Value-type patching is out of scope |
| Generic method, or method on a generic type | Harmony cannot safely patch open generics |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## How a Reload Applies

1. Resolves each file to its compiled assembly via `CompilationPipeline`, then groups the files by that assembly. Each group runs the worker once and produces one shim assembly, so the members one file adds are visible to the bodies edited in its siblings; groups are processed one after another and never affect each other.
2. Rewrites each editable method body of the group into a static shim in an out-of-process Roslyn worker. When an async, iterator, lambda, local-function, or LINQ-query body touches private/internal members, those accesses are rewritten to accessor delegates so the body can compile and run from the shim assembly (the delegation shape in step 4).
2. Rewrites each editable method body of the group into a static shim in an out-of-process Roslyn worker. When an async, iterator, lambda, local-function, or LINQ-query body touches private/internal members, those accesses are rewritten to accessor delegates so the body can compile and run from the shim assembly (the delegation shape in step 4). An `internal` member of a type the reload was not given cannot be rewritten this way, so such a body is `Skipped` (`scope-and-limits.md`).
3. Compiles the group's shims into one assembly against publicized reference copies, loads the result into the Editor domain, and binds every shim type's accessor delegates (`__BindAccessors`) before any patch is applied.
4. Patches each original method with a Harmony transpiler (ID `io.github.hatayama.uloop.hot-reload`) in one of two shapes: transplant copies the shim's IL into the original method, while delegation rewrites the original to forward its arguments to the shim, which runs as normally compiled code.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,8 @@ source on disk. When a run skips a method it had patched before, `Warnings` name
| Method on a `partial` type when another part of the type changed since the last compile and was not passed, or when a file that names the type has syntax errors (passed or not) | Hot reload binds against the compiled type; pass that file with `--files` too, or run `uloop compile`. For a file with syntax errors, fix it and run hot reload again |
| Method on a `partial` type when the other parts could not be checked against the last compile (no source snapshot yet, or more than 50 changed files in the assembly) | Run `uloop compile` |
| Method on a `partial` type whose body names a member no source file of the assembly declares | A part generated at compile time (a source generator's output) is not visible to hot reload; run `uloop compile` |
| Method or getter on a `partial` type whose body uses an `internal` member of a type the reload was not given, by its bare name, as a method passed as a delegate, inside a lambda, local function, query, iterator or async method, in a body where a lambda, local function or query works with the member's result, or in a body patched through a delegating shim | Hot reload reaches such a member only as a field, a property or a method call in the method's own statements, written with its receiver (`this.Name`, `Type.Name`, `value.Name`); qualify a bare name, or run `uloop compile` |
| Method or getter whose body uses an `internal` member of a type the reload was not given, by its bare name, inside a lambda, local function, query, iterator or async method, in a body where a lambda, local function or query works with a value hot reload could not resolve (such as the member's result), or in a body patched through a delegating shim | Hot reload reaches such a member only in the method's own statements, written with its receiver (`this.Name`, `Type.Name`, `value.Name`); qualify a bare name, or run `uloop compile` |
| Method or getter on a `partial` type whose body passes an `internal` method of such a type as a delegate, uses an `internal` event of it, or names an `internal` member of it in an object initializer or a property pattern | On a `partial` type hot reload patches such a member only as a field, a property or a method call written with its receiver; run `uloop compile` |
| Method or getter on a `partial` type whose body uses an `internal` member of a type in another assembly (through `InternalsVisibleTo`) | Reported as a name no source file of the `partial` type declares; hot reload does not patch this use from a `partial` type yet. Run `uloop compile` |
| Method on a struct (value type) | Value-type patching is out of scope |
| Generic method, or method on a generic type | Harmony cannot safely patch open generics |
Expand Down
24 changes: 24 additions & 0 deletions Assets/Tests/Editor/HotReload/HotReloadInternalMemberHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,30 @@ internal static HotReloadInternalMemberHost[] InternalHosts()
return new HotReloadInternalMemberHost[] { new HotReloadInternalMemberHost() };
}

internal event System.Action InternalEvent;

/// <summary>
/// Calls the handlers subscribed to <see cref="InternalEvent"/> and returns how many there
/// were, so a test can tell that a subscription took effect.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
public int RaiseInternalEvent()
{
System.Action handlers = InternalEvent;
if (handlers == null)
{
return 0;
}

handlers();
return handlers.GetInvocationList().Length;
}

[MethodImpl(MethodImplOptions.NoInlining)]
public static void NoOp()
{
}

/// <summary>
/// A type nested in the host, so a test can name an internal member through the nested type.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,14 @@ public async System.Threading.Tasks.Task<int> AsyncValue()
await System.Threading.Tasks.Task.CompletedTask;
return 50;
}

public event Action DerivedEvent;

[MethodImpl(MethodImplOptions.NoInlining)]
public int RaiseDerivedEvent()
{
DerivedEvent?.Invoke();
return 60;
}
}
}
17 changes: 17 additions & 0 deletions Assets/Tests/Editor/HotReload/HotReloadPlainDerivedFixture.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
Expand Down Expand Up @@ -50,5 +51,21 @@ public int DerivedProperty
[MethodImpl(MethodImplOptions.NoInlining)]
get { return 40; }
}

[MethodImpl(MethodImplOptions.NoInlining)]
public async System.Threading.Tasks.Task<int> AsyncValue()
{
await System.Threading.Tasks.Task.CompletedTask;
return 50;
}

public event Action DerivedEvent;

[MethodImpl(MethodImplOptions.NoInlining)]
public int RaiseDerivedEvent()
{
DerivedEvent?.Invoke();
return 60;
}
}
}
Loading
Loading