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))