Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
- name: Build
run: dotnet build -c:Release

- name: Tests - net10.0 (Latest)
run: dotnet run --no-build -c:Release -f:net10.0 --project test/FastExpressionCompiler.TestsRunner/FastExpressionCompiler.TestsRunner.csproj

- name: Tests - net9.0 (Latest)
run: dotnet run --no-build -c:Release -f:net9.0 --project test/FastExpressionCompiler.TestsRunner/FastExpressionCompiler.TestsRunner.csproj

Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>

<NoWarn>IDE0251;IDE0079;IDE0047;NETSDK1212</NoWarn>
<NoWarn>IDE0251;IDE0079;IDE0047;NETSDK1212;NU1510</NoWarn>

<!-- When set, reducec number of the TargetPlatforms to speedup a local Dev -->
<DevMode>false</DevMode>
Expand Down
5 changes: 5 additions & 0 deletions build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ echo:## Finished: RESTORE and BUILD

echo:
echo:## Starting: TESTS...
echo:
echo:running on .NET 10.0 (Latest)
dotnet run --no-build -f:net10.0 -c:Release --project test/FastExpressionCompiler.TestsRunner
if %ERRORLEVEL% neq 0 goto :error

echo:
echo:running on .NET 9.0 (Latest)
dotnet run --no-build -f:net9.0 -c:Release --project test/FastExpressionCompiler.TestsRunner
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0;net10.0</TargetFrameworks>

<VersionPrefix>5.4.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
Expand Down
47 changes: 47 additions & 0 deletions src/FastExpressionCompiler/FastExpressionCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9295,6 +9295,15 @@ public virtual LocalBuilder DeclareLocal(Type localType, bool pinned)
return localBuilder;
}
*/
#if NET10_0_OR_GREATER
// In .NET 10+, use UnsafeAccessorType to directly access RuntimeILGenerator's private fields
// without the need for reflection-based DynamicMethod generation at startup
GetNextLocalVarLocation = static (il, t) =>
{
GetMLocalSignature(il).AddArgument(t, false);
return PostInc(ref GetMLocalCount(il));
};
#else
// Let's try to acquire the more efficient less allocating method
var m_localSignatureField = DynamicILGeneratorType.GetField("m_localSignature", instanceNonPublic);
if (m_localSignatureField == null)
Expand Down Expand Up @@ -9340,6 +9349,7 @@ public virtual LocalBuilder DeclareLocal(Type localType, bool pinned)

ExpressionCompiler.FreePooledParamTypes(paramTypes);
endOfGetNextVar:;
#endif
}

// Restore the demit
Expand Down Expand Up @@ -9465,6 +9475,43 @@ public override void Emit(OpCode opcode, MethodInfo meth, int paramCount)
m_length += 4;
}
*/

#if NET10_0_OR_GREATER
// UnsafeAccessorType methods for accessing private fields of the non-public RuntimeILGenerator class.
// RuntimeILGenerator is the internal base class of DynamicILGenerator that holds the core IL generation state.
// Using UnsafeAccessorType avoids reflection at call time and is compatible with AOT compilation.

/// <summary>Gets a ref to the <c>m_localSignature</c> field of the ILGenerator (declared in the internal RuntimeILGenerator).</summary>
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_localSignature")]
internal static extern ref SignatureHelper GetMLocalSignature(
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);

/// <summary>Gets a ref to the <c>m_localCount</c> field of the ILGenerator (declared in the internal RuntimeILGenerator).</summary>
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_localCount")]
internal static extern ref int GetMLocalCount(
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);

/// <summary>Gets a ref to the <c>m_length</c> field of the ILGenerator (declared in the internal RuntimeILGenerator).</summary>
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_length")]
internal static extern ref int GetMLength(
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);

/// <summary>Gets a ref to the <c>m_ILStream</c> field of the ILGenerator (declared in the internal RuntimeILGenerator).</summary>
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_ILStream")]
internal static extern ref byte[] GetMILStream(
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);

/// <summary>Calls the internal <c>UpdateStackSize</c> method on the ILGenerator (declared in the internal RuntimeILGenerator).</summary>
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "UpdateStackSize")]
internal static extern void UpdateStackSize(
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il,
OpCode opcode, int stackchange);

/// <summary>Gets a ref to the <c>m_tokens</c> field on a DynamicScope instance (internal type).</summary>
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_tokens")]
internal static extern ref System.Collections.Generic.List<object> GetMTokens(
[UnsafeAccessorType("System.Reflection.Emit.DynamicScope")] object scope);
#endif
}

[RequiresUnreferencedCode(Trimming.Message)]
Expand Down
4 changes: 2 additions & 2 deletions src/FastExpressionCompiler/FastExpressionCompiler.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0;net10.0</TargetFrameworks>

<VersionPrefix>5.4.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
Expand Down
153 changes: 134 additions & 19 deletions test/FastExpressionCompiler.IssueTests/EmitHacksTest.cs
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot what are we testing in EmitHacks. Is it something that we already using in Fec or something notvyrt applied. If latter - can ee apply it now with the fixes in place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EmitHacksTest demonstrates direct IL stream manipulation — writing bytes straight to m_ILStream (and updating m_length/stack tracking via UpdateStackSize) instead of going through ILGenerator.Emit(). This is a future optimization path that is not yet applied to FEC's main emission loop.

What IS already applied in DynamicMethodHacks:

  • GetNextLocalVarLocation uses UAT (NET10+) / a startup-generated DynamicMethod (NET8–9) to access m_localSignature and m_localCount to track locals — bypassing the overhead of ILGenerator.DeclareLocal().

What EmitHacksTest proves but FEC doesn't yet use:

  • Bypassing ILGenerator.Emit() entirely and writing opcodes/operands directly into m_ILStream. This would be a significant refactor of every emission site in FEC and is best tracked as a separate performance issue (e.g. issue Hack DynamicMethod #351 is already referenced in the hack comments).

Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ public class EmitHacksTest : ITest
public int Run()
{
DynamicMethod_Emit_Hack();
// DynamicMethod_Emit_Newobj();
// DynamicMethod_Hack_Emit_Newobj();
#if NET10_0_OR_GREATER
DynamicMethod_Emit_Hack_Net10();
return 4;
#else
return 3;
#endif
}

public void DynamicMethod_Emit_Hack()
Expand All @@ -27,18 +30,27 @@ public void DynamicMethod_Emit_Hack()
Asserts.AreEqual(42, a);
}

static Type ilType = typeof(ILGenerator).Assembly.GetType("System.Reflection.Emit.DynamicILGenerator");
static FieldInfo mScopeField = ilType.GetField("m_scope", BindingFlags.Instance | BindingFlags.NonPublic);
static readonly Type ilType = typeof(ILGenerator).Assembly.GetType("System.Reflection.Emit.DynamicILGenerator");

static Type scopeType = ilType.Assembly.GetType("System.Reflection.Emit.DynamicScope");
static FieldInfo mTokensField = scopeType.GetField("m_tokens", BindingFlags.Instance | BindingFlags.NonPublic);
// m_scope field is on DynamicILGenerator (internal class) - accessed via reflection since
// the field type DynamicScope is also internal (UnsafeAccessorType can't return non-public types).
static readonly FieldInfo mScopeField = ilType?.GetField("m_scope", BindingFlags.Instance | BindingFlags.NonPublic);

static FieldInfo mLengthField = typeof(ILGenerator).GetField("m_length", BindingFlags.Instance | BindingFlags.NonPublic);
static FieldInfo mILStreamField = typeof(ILGenerator).GetField("m_ILStream", BindingFlags.Instance | BindingFlags.NonPublic);
static MethodInfo updateStackSize = typeof(ILGenerator).GetMethod("UpdateStackSize", BindingFlags.Instance | BindingFlags.NonPublic);
static readonly Type scopeType = ilType?.Assembly.GetType("System.Reflection.Emit.DynamicScope");
static readonly FieldInfo mTokensField = scopeType?.GetField("m_tokens", BindingFlags.Instance | BindingFlags.NonPublic);

// m_length, m_ILStream, and UpdateStackSize are on RuntimeILGenerator (the internal base class of DynamicILGenerator),
// NOT on the public ILGenerator class. Look up the fields on the correct type.
static readonly Type runtimeILGenType = ilType?.BaseType; // System.Reflection.Emit.RuntimeILGenerator
static readonly FieldInfo mLengthField = runtimeILGenType?.GetField("m_length", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
static readonly FieldInfo mILStreamField = runtimeILGenType?.GetField("m_ILStream", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
static readonly MethodInfo updateStackSizeMethod = runtimeILGenType?.GetMethod("UpdateStackSize", BindingFlags.Instance | BindingFlags.NonPublic);

private static Func<ILGenerator, IList<object>> GetScopeTokens()
{
if (mScopeField == null || mTokensField == null)
return null;

var dynMethod = new DynamicMethod(string.Empty,
typeof(IList<object>), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(ILGenerator) },
typeof(ExpressionCompiler), skipVisibility: true);
Expand All @@ -57,6 +69,8 @@ private static Func<ILGenerator, IList<object>> GetScopeTokens()

private static GetFieldRefDelegate<TFieldHolder, TField> CreateFieldAccessor<TFieldHolder, TField>(FieldInfo field)
{
if (field == null) return null;

var dynMethod = new DynamicMethod(string.Empty,
typeof(TField).MakeByRefType(), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(TFieldHolder) },
typeof(TFieldHolder), skipVisibility: true);
Expand All @@ -69,14 +83,34 @@ private static GetFieldRefDelegate<TFieldHolder, TField> CreateFieldAccessor<TFi
return (GetFieldRefDelegate<TFieldHolder, TField>)dynMethod.CreateDelegate(typeof(GetFieldRefDelegate<TFieldHolder, TField>));
}

static GetFieldRefDelegate<ILGenerator, int> mLengthFieldAccessor = CreateFieldAccessor<ILGenerator, int>(mLengthField);
static GetFieldRefDelegate<ILGenerator, byte[]> mILStreamAccessor = CreateFieldAccessor<ILGenerator, byte[]>(mILStreamField);
static readonly GetFieldRefDelegate<ILGenerator, int> mLengthFieldAccessor = CreateFieldAccessor<ILGenerator, int>(mLengthField);
static readonly GetFieldRefDelegate<ILGenerator, byte[]> mILStreamAccessor = CreateFieldAccessor<ILGenerator, byte[]>(mILStreamField);

static readonly Action<ILGenerator, OpCode, int> updateStackSizeDelegate = GetUpdateStackSizeDelegate();

static Action<ILGenerator, OpCode, int> updateStackSizeDelegate =
(Action<ILGenerator, OpCode, int>)Delegate.CreateDelegate(typeof(Action<ILGenerator, OpCode, int>), null, updateStackSize);
private static Action<ILGenerator, OpCode, int> GetUpdateStackSizeDelegate()
{
if (updateStackSizeMethod == null) return null;
// Cannot use Delegate.CreateDelegate with a method from a non-public declaring type (RuntimeILGenerator).
// Instead, wrap the call in a DynamicMethod with skipVisibility: true.
var dynMethod = new DynamicMethod(string.Empty,
typeof(void), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(ILGenerator), typeof(OpCode), typeof(int) },
typeof(ExpressionCompiler), skipVisibility: true);
var il = dynMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_1); // ILGenerator (runtime type: DynamicILGenerator : RuntimeILGenerator)
il.Emit(OpCodes.Ldarg_2); // OpCode
il.Emit(OpCodes.Ldarg_3); // int stackchange
il.Emit(OpCodes.Call, updateStackSizeMethod);
il.Emit(OpCodes.Ret);
return (Action<ILGenerator, OpCode, int>)dynMethod.CreateDelegate(
typeof(Action<ILGenerator, OpCode, int>), ExpressionCompiler.EmptyArrayClosure);
}

public static Func<int, int> Get_DynamicMethod_Emit_Hack()
{
if (mLengthFieldAccessor == null || mILStreamAccessor == null || updateStackSizeDelegate == null || getScopeTokens == null)
return null;

var meth = MethodStatic1Arg;
var paramCount = 1;

Expand Down Expand Up @@ -128,6 +162,87 @@ public static Func<int, int> Get_DynamicMethod_Emit_Hack()
return (Func<int, int>)dynMethod.CreateDelegate(typeof(Func<int, int>), ExpressionCompiler.EmptyArrayClosure);
}

#if NET10_0_OR_GREATER
// In .NET 10+, use UnsafeAccessorType to access the private fields of non-public types directly,
// without the DynamicMethod-based delegation used in earlier .NET versions.
// RuntimeILGenerator is the internal base class of DynamicILGenerator that holds the IL stream state.

/// <summary>Directly accesses m_length on RuntimeILGenerator via UnsafeAccessorType (NET10+).</summary>
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_length")]
private static extern ref int GetMLength_Net10(
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);

/// <summary>Directly accesses m_ILStream on RuntimeILGenerator via UnsafeAccessorType (NET10+).</summary>
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_ILStream")]
private static extern ref byte[] GetMILStream_Net10(
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);

/// <summary>Directly calls UpdateStackSize on RuntimeILGenerator via UnsafeAccessorType (NET10+).</summary>
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "UpdateStackSize")]
private static extern void UpdateStackSize_Net10(
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il,
OpCode opcode, int stackchange);

/// <summary>Directly accesses m_tokens on DynamicScope via UnsafeAccessorType (NET10+).</summary>
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_tokens")]
private static extern ref List<object> GetMTokens_Net10(
[UnsafeAccessorType("System.Reflection.Emit.DynamicScope")] object scope);

public void DynamicMethod_Emit_Hack_Net10()
{
var f = Get_DynamicMethod_Emit_Hack_Net10();
var a = f(41);
Asserts.AreEqual(42, a);
}

/// <summary>
/// Demonstrates using UnsafeAccessorType (NET10+) to directly access private fields
/// of non-public types (RuntimeILGenerator, DynamicScope) for fast IL emission.
/// Replaces the DynamicMethod-based delegation approach used in earlier .NET versions.
/// </summary>
public static Func<int, int> Get_DynamicMethod_Emit_Hack_Net10()
{
var meth = MethodStatic1Arg;
var paramCount = 1;

var dynMethod = new DynamicMethod(string.Empty,
typeof(int), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(int) },
typeof(ExpressionCompiler),
skipVisibility: true);

var il = dynMethod.GetILGenerator(16);

// Use UnsafeAccessorType to get refs to the internal IL stream fields directly
ref var mLength = ref GetMLength_Net10(il);
ref var mILStream = ref GetMILStream_Net10(il);

// il.Emit(OpCodes.Ldarg_1);
mILStream[mLength++] = (byte)OpCodes.Ldarg_1.Value;
UpdateStackSize_Net10(il, OpCodes.Ldarg_1, 1);

// il.Emit(OpCodes.Call, meth);
mILStream[mLength++] = (byte)OpCodes.Call.Value;
UpdateStackSize_Net10(il, OpCodes.Call, CalcStackChange(meth, paramCount));

// Access m_scope via reflection (DynamicILGenerator.m_scope returns DynamicScope which is a non-public type,
// so UnsafeAccessorType cannot currently be used for the return value).
// Then use UnsafeAccessorType to access m_tokens on the DynamicScope instance directly.
if (mScopeField == null) return null;
var scope = mScopeField.GetValue(il);
ref var mTokens = ref GetMTokens_Net10(scope);
mTokens.Add(meth.MethodHandle);
var token = mTokens.Count - 1 | (int)0x06000000; // MetadataTokenType.MethodDef
BinaryPrimitives.WriteInt32LittleEndian(mILStream.AsSpan(mLength), token);
mLength += 4;

// il.Emit(OpCodes.Ret);
mILStream[mLength++] = (byte)OpCodes.Ret.Value;
UpdateStackSize_Net10(il, OpCodes.Ret, 0);

return (Func<int, int>)dynMethod.CreateDelegate(typeof(Func<int, int>), ExpressionCompiler.EmptyArrayClosure);
}
#endif

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int CalcStackChange(MethodInfo meth, int paramCount)
{
Expand Down Expand Up @@ -213,15 +328,15 @@ public static Func<A> Get_DynamicMethod_Hack_Emit_Newobj()
// m_tokens.Add(rtConstructor.MethodHandle);
// var tk = m_tokens.Count - 1 | (int)MetadataTokenType.MethodDef;

var mScopeField = ilType.GetField("m_scope", BindingFlags.Instance | BindingFlags.NonPublic);
if (mScopeField == null)
var scopeField = ilType.GetField("m_scope", BindingFlags.Instance | BindingFlags.NonPublic);
if (scopeField == null)
return null;
var mScope = mScopeField.GetValue(il);
var mScope = scopeField.GetValue(il);

var mTokensField = mScope.GetType().GetField("m_tokens", BindingFlags.Instance | BindingFlags.NonPublic);
if (mTokensField == null)
var tokensField = mScope.GetType().GetField("m_tokens", BindingFlags.Instance | BindingFlags.NonPublic);
if (tokensField == null)
return null;
var mTokens = mTokensField.GetValue(mScope);
var mTokens = tokensField.GetValue(mScope);


il.Emit(OpCodes.Ret);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;net6.0;net8.0;net9.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;net6.0;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net9.0;net8.0;net6.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' == 'true' OR '$(Configuration)' == 'Debug'">net9.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net9.0;net8.0;net6.0;net10.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DevMode)' == 'true' OR '$(Configuration)' == 'Debug'">net9.0;net10.0</TargetFrameworks>

<OutputType>Exe</OutputType>
<IsTestProject>false</IsTestProject>
Expand Down
Loading
Loading