diff --git a/WoofWare.PawPrint.Test/TestCases.fs b/WoofWare.PawPrint.Test/TestCases.fs
index 558cbbf..564c4ff 100644
--- a/WoofWare.PawPrint.Test/TestCases.fs
+++ b/WoofWare.PawPrint.Test/TestCases.fs
@@ -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
diff --git a/WoofWare.PawPrint.Test/WoofWare.PawPrint.Test.fsproj b/WoofWare.PawPrint.Test/WoofWare.PawPrint.Test.fsproj
index f19ae6d..42d2af8 100644
--- a/WoofWare.PawPrint.Test/WoofWare.PawPrint.Test.fsproj
+++ b/WoofWare.PawPrint.Test/WoofWare.PawPrint.Test.fsproj
@@ -28,6 +28,7 @@
+
diff --git a/WoofWare.PawPrint.Test/sources/CustomDelegate.cs b/WoofWare.PawPrint.Test/sources/CustomDelegate.cs
new file mode 100644
index 0000000..07af473
--- /dev/null
+++ b/WoofWare.PawPrint.Test/sources/CustomDelegate.cs
@@ -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;
+ }
+}
diff --git a/WoofWare.PawPrint/AbstractMachine.fs b/WoofWare.PawPrint/AbstractMachine.fs
index f58eb60..4d5f528 100644
--- a/WoofWare.PawPrint/AbstractMachine.fs
+++ b/WoofWare.PawPrint/AbstractMachine.fs
@@ -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 =
diff --git a/WoofWare.PawPrint/IlMachineState.fs b/WoofWare.PawPrint/IlMachineState.fs
index d4800ec..04d4a27 100644
--- a/WoofWare.PawPrint/IlMachineState.fs
+++ b/WoofWare.PawPrint/IlMachineState.fs
@@ -455,6 +455,7 @@ module IlMachineState =
(wasInitialising : RuntimeConcreteType option)
(wasConstructing : ManagedHeapAddress option)
(wasClassConstructor : bool)
+ (advanceProgramCounterOfCaller : bool)
(methodGenerics : ImmutableArray option)
(methodToCall : WoofWare.PawPrint.MethodInfo)
(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)
(thread : ThreadId)
+ (advanceProgramCounterOfCaller : bool)
(methodGenerics : TypeDefn ImmutableArray option)
(methodToCall : WoofWare.PawPrint.MethodInfo)
(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
diff --git a/WoofWare.PawPrint/Program.fs b/WoofWare.PawPrint/Program.fs
index 74b4111..9b65908 100644
--- a/WoofWare.PawPrint/Program.fs
+++ b/WoofWare.PawPrint/Program.fs
@@ -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 =
diff --git a/WoofWare.PawPrint/UnaryMetadataIlOp.fs b/WoofWare.PawPrint/UnaryMetadataIlOp.fs
index 984da52..4077e48 100644
--- a/WoofWare.PawPrint/UnaryMetadataIlOp.fs
+++ b/WoofWare.PawPrint/UnaryMetadataIlOp.fs
@@ -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))