Implement custom delegate calling (#60)

This commit is contained in:
Patrick Stevens
2025-06-20 16:45:43 +01:00
committed by GitHub
parent c620152fb1
commit f9e03f4340
7 changed files with 226 additions and 121 deletions

View File

@@ -73,6 +73,22 @@ module TestCases =
NativeImpls = MockEnv.make ()
LocalVariablesOfMain = [ CliType.Numeric (CliNumericType.Int32 1) ]
}
{
FileName = "CustomDelegate.cs"
ExpectedReturnCode = 8
NativeImpls = MockEnv.make ()
LocalVariablesOfMain =
[
// filter
CliType.ObjectRef (Some (ManagedHeapAddress 2))
// result
CliType.OfBool true
// result, cloned for "if(result)" check
CliType.OfBool true
// ret
CliType.Numeric (CliNumericType.Int32 8)
]
}
{
FileName = "ArgumentOrdering.cs"
ExpectedReturnCode = 42

View File

@@ -28,6 +28,7 @@
<EmbeddedResource Include="sources\Threads.cs" />
<EmbeddedResource Include="sources\ResizeArray.cs" />
<EmbeddedResource Include="sources\ArgumentOrdering.cs" />
<EmbeddedResource Include="sources\CustomDelegate.cs" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,27 @@
public delegate bool MyFilter(object item, object criteria);
public class DelegateDemo
{
// This static field initialization will generate the exact IL pattern:
public static readonly MyFilter FilterField = FilterImpl;
// The static method that the delegate points to
private static bool FilterImpl(object item, object criteria)
{
return true;
}
public static int Main(string[] argv)
{
// Force static constructor to run
var filter = FilterField;
// Test the delegate
bool result = filter("test item", "criterion");
if (result)
{
return 8;
}
return 5;
}
}

View File

@@ -1,5 +1,6 @@
namespace WoofWare.PawPrint
open System.Collections.Immutable
open Microsoft.Extensions.Logging
open Microsoft.FSharp.Core
open WoofWare.PawPrint.ExternImplementations
@@ -21,7 +22,6 @@ module AbstractMachine =
match instruction.ExecutingMethod.Instructions with
| None ->
// TODO: this could be a delegate, like System.Func.
let targetAssy =
state.LoadedAssembly instruction.ExecutingMethod.DeclaringType.Assembly
|> Option.get
@@ -36,17 +36,70 @@ module AbstractMachine =
targetAssy.Name
targetType.BaseType
match baseType, instruction.ReturnState with
| ResolvedBaseType.Delegate,
Some {
WasConstructingObj = Some _
} ->
IlMachineState.executeDelegateConstructor instruction state
// can't advance the program counter here - there's no IL instructions executing!
|> IlMachineState.returnStackFrame loggerFactory baseClassTypes thread
|> Option.get
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
match baseType with
| ResolvedBaseType.Delegate ->
match instruction.ReturnState with
| None -> failwith "How come we don't have a return point from a delegate?!"
| Some {
WasConstructingObj = Some _
} ->
IlMachineState.executeDelegateConstructor instruction state
// can't advance the program counter here - there's no IL instructions executing!
|> IlMachineState.returnStackFrame loggerFactory baseClassTypes thread
|> Option.get
|> Tuple.withRight WhatWeDid.Executed
|> ExecutionResult.Stepped
| Some {
WasConstructingObj = None
} ->
// We've been instructed to run a delegate.
let delegateToRunAddr =
match instruction.Arguments.[0] with
| CliType.ObjectRef (Some addr) -> addr
| _ -> failwith "expected a managed object ref to delegate"
let delegateToRun = state.ManagedHeap.NonArrayObjects.[delegateToRunAddr]
if delegateToRun.Fields.["_target"] <> CliType.ObjectRef None then
failwith "TODO: delegate target wasn't None"
let methodPtr =
match delegateToRun.Fields.["_methodPtr"] with
| CliType.Numeric (CliNumericType.ProvenanceTrackedNativeInt64 mi) -> mi
| _ -> failwith "unexpectedly not a method pointer in delegate invocation"
let typeGenerics =
instruction.ExecutingMethod.DeclaringType.Generics |> ImmutableArray.CreateRange
let methodGenerics = instruction.ExecutingMethod.Generics
let methodPtr =
methodPtr |> MethodInfo.mapTypeGenerics (fun i _ -> typeGenerics.[i])
// When we return, we need to go back up the stack
match state |> IlMachineState.returnStackFrame loggerFactory baseClassTypes thread with
| None -> failwith "unexpectedly nowhere to return from delegate"
| Some state ->
// Push args
let state =
(state, instruction.Arguments)
||> Seq.fold (fun state arg -> IlMachineState.pushToEvalStack arg thread state)
// Don't advance the program counter again on return; that was already done by the Callvirt that
// caused this delegate to be invoked.
let state, result =
state
|> IlMachineState.callMethodInActiveAssembly
loggerFactory
baseClassTypes
thread
false
(Some methodGenerics)
methodPtr
None
ExecutionResult.Stepped (state, result)
| _ ->
let outcome =

View File

@@ -455,6 +455,7 @@ module IlMachineState =
(wasInitialising : RuntimeConcreteType option)
(wasConstructing : ManagedHeapAddress option)
(wasClassConstructor : bool)
(advanceProgramCounterOfCaller : bool)
(methodGenerics : ImmutableArray<TypeDefn> option)
(methodToCall : WoofWare.PawPrint.MethodInfo<TypeDefn, WoofWare.PawPrint.GenericParameter>)
(thread : ThreadId)
@@ -464,6 +465,7 @@ module IlMachineState =
=
let activeAssy = state.ActiveAssembly thread
// Check for intrinsics first
let isIntrinsic =
methodToCall.IsJITIntrinsic
(fun handle ->
@@ -473,13 +475,12 @@ module IlMachineState =
)
activeAssy.Methods
let handleIntrinsic =
match
if isIntrinsic then
callIntrinsic corelib methodToCall state
else
None
match handleIntrinsic with
with
| Some result -> result
| None ->
@@ -488,6 +489,7 @@ module IlMachineState =
| [] -> None
| x -> Some (ImmutableArray.CreateRange x)
// Get zero values for all parameters
let state, argZeroObjects =
((state, []), methodToCall.Signature.ParameterTypes)
||> List.fold (fun (state, zeros) ty ->
@@ -512,122 +514,108 @@ module IlMachineState =
methodToCall
|> MethodInfo.mapMethodGenerics (fun _ param -> methodGenerics.Value.[param.SequenceNumber])
let state, newFrame, oldFrame =
// Helper to pop and coerce a single argument
let popAndCoerceArg zeroType methodState =
let value, newState = MethodState.popFromStack methodState
EvalStackValue.toCliTypeCoerced zeroType value, newState
// Collect arguments based on calling convention
let args, afterPop =
if methodToCall.IsStatic then
let args = ImmutableArray.CreateBuilder methodToCall.Parameters.Length
let mutable afterPop = activeMethodState
// Static method: pop args in reverse order
let args = ImmutableArray.CreateBuilder (methodToCall.Parameters.Length)
let mutable currentState = activeMethodState
for i = 0 to methodToCall.Parameters.Length - 1 do
let poppedArg, afterPop' = afterPop |> MethodState.popFromStack
let zeroArg = argZeroObjects.[i]
let poppedArg = EvalStackValue.toCliTypeCoerced zeroArg poppedArg
afterPop <- afterPop'
args.Add poppedArg
for i = methodToCall.Parameters.Length - 1 downto 0 do
let arg, newState = popAndCoerceArg argZeroObjects.[i] currentState
args.Add arg
currentState <- newState
args.Reverse ()
let rec newFrame (state : IlMachineState) =
let meth =
MethodState.Empty
corelib
state._LoadedAssemblies
(state.ActiveAssembly thread)
methodToCall
methodGenerics
(args.ToImmutable ())
(Some
{
JumpTo = threadState.ActiveMethodState
WasInitialisingType = wasInitialising
WasConstructingObj = wasConstructing
})
match meth with
| Ok r -> state, r
| Error toLoad ->
(state, toLoad)
||> List.fold (fun state (toLoad : WoofWare.PawPrint.AssemblyReference) ->
let state, _, _ =
loadAssembly
loggerFactory
(state.LoadedAssembly methodToCall.DeclaringType.Assembly |> Option.get)
(fst toLoad.Handle)
state
state
)
|> newFrame
let state, newFrame = newFrame state
let oldFrame =
if wasClassConstructor then
afterPop
else
afterPop |> MethodState.advanceProgramCounter
state, newFrame, oldFrame
args.ToImmutable (), currentState
else
let args = ImmutableArray.CreateBuilder (methodToCall.Parameters.Length + 1)
// Instance method: handle `this` pointer
let argCount = methodToCall.Parameters.Length
let args = ImmutableArray.CreateBuilder (argCount + 1)
let mutable currentState = activeMethodState
let thisPointer, afterPop = activeMethodState |> MethodState.popFromStack
let mutable afterPop = afterPop
match wasConstructing with
| Some _ ->
// Constructor: `this` is on top of stack, by our own odd little calling convention
// where Newobj puts the object pointer on top
let thisArg, newState =
popAndCoerceArg (CliType.RuntimePointer (CliRuntimePointer.Unmanaged ())) currentState
for i = 1 to methodToCall.Parameters.Length do
let poppedArg, afterPop' = afterPop |> MethodState.popFromStack
let zeroArg = argZeroObjects.[i - 1]
args.Add thisArg
currentState <- newState
let poppedArg = EvalStackValue.toCliTypeCoerced zeroArg poppedArg
afterPop <- afterPop'
args.Add poppedArg
// Pop remaining args in reverse
for i = argCount - 1 downto 0 do
let arg, newState = popAndCoerceArg argZeroObjects.[i] currentState
args.Add (arg)
currentState <- newState
// it only matters that the RuntimePointer is a RuntimePointer, so that the coercion has a target of the
// right shape
args.Add (
EvalStackValue.toCliTypeCoerced
(CliType.RuntimePointer (CliRuntimePointer.Unmanaged ()))
thisPointer
)
args.ToImmutable (), currentState
| None ->
// Regular instance method: args then `this`
for i = argCount - 1 downto 0 do
let arg, newState = popAndCoerceArg argZeroObjects.[i] currentState
args.Add (arg)
currentState <- newState
args.Reverse ()
let thisArg, newState =
popAndCoerceArg (CliType.RuntimePointer (CliRuntimePointer.Unmanaged ())) currentState
let rec newFrame (state : IlMachineState) =
let meth =
MethodState.Empty
corelib
state._LoadedAssemblies
(state.ActiveAssembly thread)
methodToCall
methodGenerics
(args.ToImmutable ())
(Some
{
JumpTo = threadState.ActiveMethodState
WasInitialisingType = wasInitialising
WasConstructingObj = wasConstructing
})
args.Add thisArg
currentState <- newState
match meth with
| Ok r -> state, r
| Error toLoad ->
(state, toLoad)
||> List.fold (fun state (toLoad : WoofWare.PawPrint.AssemblyReference) ->
let state, _, _ =
loadAssembly
loggerFactory
(state.LoadedAssembly methodToCall.DeclaringType.Assembly |> Option.get)
(fst toLoad.Handle)
state
args.Reverse ()
args.ToImmutable (), currentState
state
)
|> newFrame
// Helper to create new frame with assembly loading
let rec createNewFrame state =
let returnInfo =
Some
{
JumpTo = threadState.ActiveMethodState
WasInitialisingType = wasInitialising
WasConstructingObj = wasConstructing
}
let state, newFrame = newFrame state
let oldFrame = afterPop |> MethodState.advanceProgramCounter
state, newFrame, oldFrame
match
MethodState.Empty
corelib
state._LoadedAssemblies
(state.ActiveAssembly thread)
methodToCall
methodGenerics
args
returnInfo
with
| Ok frame -> state, frame
| Error toLoad ->
let state' =
(state, toLoad)
||> List.fold (fun s (asmRef : WoofWare.PawPrint.AssemblyReference) ->
let s, _, _ =
loadAssembly
loggerFactory
(state.LoadedAssembly methodToCall.DeclaringType.Assembly |> Option.get)
(fst asmRef.Handle)
s
s
)
createNewFrame state'
let state, newFrame = createNewFrame state
let oldFrame =
if wasClassConstructor || not advanceProgramCounterOfCaller then
afterPop
else
afterPop |> MethodState.advanceProgramCounter
let newThreadState =
{ threadState with
@@ -766,6 +754,7 @@ module IlMachineState =
(Some ty)
None
true
true
// constructor is surely not generic
None
cctorMethod
@@ -804,10 +793,15 @@ module IlMachineState =
else
state, WhatWeDid.BlockedOnClassInit threadId
/// It may be useful to *not* advance the program counter of the caller, e.g. if you're using `callMethodInActiveAssembly`
/// as a convenient way to move to a different method body rather than to genuinely perform a call.
/// (Delegates do this, for example: we get a call to invoke the delegate, and then we implement the delegate as
/// another call to its function pointer.)
let callMethodInActiveAssembly
(loggerFactory : ILoggerFactory)
(corelib : BaseClassTypes<DumpedAssembly>)
(thread : ThreadId)
(advanceProgramCounterOfCaller : bool)
(methodGenerics : TypeDefn ImmutableArray option)
(methodToCall : WoofWare.PawPrint.MethodInfo<TypeDefn, WoofWare.PawPrint.GenericParameter>)
(weAreConstructingObj : ManagedHeapAddress option)
@@ -827,6 +821,7 @@ module IlMachineState =
None
weAreConstructingObj
false
advanceProgramCounterOfCaller
methodGenerics
methodToCall
thread
@@ -1175,12 +1170,12 @@ module IlMachineState =
constructed.Type.BaseType
match resolvedBaseType with
| ResolvedBaseType.Delegate
| ResolvedBaseType.Object -> state |> pushToEvalStack (CliType.OfManagedObject constructing) currentThread
| ResolvedBaseType.ValueType ->
state
|> pushToEvalStack (CliType.ValueType (Seq.toList constructed.Fields.Values)) currentThread
| ResolvedBaseType.Enum -> failwith "TODO"
| ResolvedBaseType.Delegate -> failwith "TODO"
| None ->
match threadStateAtEndOfMethod.MethodState.EvaluationStack.Values with
| [] ->
@@ -1241,9 +1236,9 @@ module IlMachineState =
let executeDelegateConstructor (instruction : MethodState) (state : IlMachineState) : IlMachineState =
// We've been called with arguments already popped from the stack into local arguments.
let constructing = instruction.Arguments.[2]
let constructing = instruction.Arguments.[0]
let methodPtr = instruction.Arguments.[1]
let targetObj = instruction.Arguments.[0]
let targetObj = instruction.Arguments.[2]
let targetObj =
match targetObj with

View File

@@ -182,6 +182,8 @@ module Program =
let state, _ =
pumpToReturn loggerFactory logger baseClassTypes impls mainThread state
logger.LogInformation "Main method class now initialised"
// Now that BCL initialisation has taken place and the user-code classes are constructed,
// overwrite the main thread completely.
let methodState =

View File

@@ -76,6 +76,7 @@ module internal UnaryMetadataIlOp =
loggerFactory
baseClassTypes
thread
true
methodGenerics
methodToCall
None
@@ -138,7 +139,7 @@ module internal UnaryMetadataIlOp =
state.WithThreadSwitchedToAssembly method.DeclaringType.Assembly thread
|> fst
|> IlMachineState.callMethodInActiveAssembly loggerFactory baseClassTypes thread generics method None
|> IlMachineState.callMethodInActiveAssembly loggerFactory baseClassTypes thread true generics method None
| Castclass -> failwith "TODO: Castclass unimplemented"
| Newobj ->
@@ -227,6 +228,7 @@ module internal UnaryMetadataIlOp =
loggerFactory
baseClassTypes
thread
true
None
ctor
(Some allocatedAddr)
@@ -880,11 +882,20 @@ module internal UnaryMetadataIlOp =
failwith "TODO: Ldsflda - push unmanaged pointer"
| Ldftn ->
let logger = loggerFactory.CreateLogger "Ldftn"
let method =
match metadataToken with
| MetadataToken.MethodDef handle -> activeAssy.Methods.[handle]
| t -> failwith $"Unexpectedly asked to Ldftn a non-method: {t}"
logger.LogDebug (
"Pushed pointer to function {LdFtnAssembly}.{LdFtnType}.{LdFtnMethodName}",
method.DeclaringType.Assembly.Name,
method.DeclaringType.Name,
method.Name
)
state
|> IlMachineState.pushToEvalStack'
(EvalStackValue.NativeInt (NativeIntSource.FunctionPointer method))