private unsafe void HandleInvokeFunctionFromNative(IntPtr obj, FFrame stack, IntPtr result, UFunction.FuncInvokerManaged managedFunctionInvoker) { IntPtr function = stack.CurrentNativeFunction; IntPtr paramsBuffer = stack.Locals; if (managedFunctionInvoker != null) { // Call the managed function invoker which will marshal the params from the native params buffer and then call the // target managed function managedFunctionInvoker(paramsBuffer, obj); } // Copy out params back from the locals buffer if (Native_UFunction.HasAnyFunctionFlags(function, EFunctionFlags.HasOutParms)) { // This assumes that UProperty will be itterated in the exact same order as the caller created stack->OutParms // (we could iterate stack->OutParms until nullptr but it isn't null terminated on release builds) FOutParmRec *outParms = stack.OutParmsPtr; foreach (IntPtr paramProp in new NativeReflection.NativeFieldIterator(Runtime.Classes.UProperty, function, false)) { EPropertyFlags paramFlags = Native_UProperty.GetPropertyFlags(paramProp); if ((paramFlags & EPropertyFlags.OutParm) == EPropertyFlags.OutParm && (paramFlags & EPropertyFlags.ConstParm) != EPropertyFlags.ConstParm) { Debug.Assert(outParms->Property == paramProp); // - REMOVING the DestroyValue call. The issue with this DestroyValue call is that PropAddr // and paramsBuffer can (will?) be refering to the same data which we should have set in // managedFunctionInvoker. So a call to DestroyValue would be destroying the data we want! // Though if PropAddr is for some reason still holding onto unknown memory this will leak // memory and we will need to investigate further. // - The "refering to the same data" is because of the params Memcpy in UObject::ProcessEvent. // They wont have the same address but their inital data will be the same and as such // things like FString data pointer would be pointing to the same address. // - We may still need to call DestroyValue in the BP/VM code path above. // Destroy the existing memory (this assumed the existing memory is valid or at least memzerod) //Native_UProperty.DestroyValue(paramProp, outParms->PropAddr); // A raw memcpy should be OK since the managed invoker should have set this memory appropriately. FMemory.Memcpy(outParms->PropAddr, paramsBuffer + Native_UProperty.GetOffset_ForUFunction(paramProp), Native_UProperty.Get_ElementSize(paramProp));// Should be ArrayDim*ElementSize but ArrayDim should always be 1 for params outParms = outParms->NextOutParamPtr; } } } // We assume that the caller will clean up the memory held by stack->Locals so we don't iterate over the // DestructorLink as we do in HandleInvokeFunctionFromBP. HandleInvokeFunctionFromBP needs to as it creates // copies of data when calling stack->Step() which don't occur here as we use the existing stack->Locals buffer. }
private unsafe void HandleInvokeFunctionFromBP(IntPtr obj, FFrame stack, IntPtr result, UFunction.FuncInvokerManaged managedFunctionInvoker) { // NOTE: ScriptCore.cpp uses PropertiesSize instead of ParamsSize. Is it ever any different? (it says alignment // may make them different) If it is different we should probably use PropertiesSize (including in generated code / // IL) as ScriptCore.cpp uses a memcpy of our memory. Debug.Assert(Native_UStruct.Get_PropertiesSize(stack.CurrentNativeFunction) == Native_UFunction.Get_ParmsSize(stack.CurrentNativeFunction)); IntPtr function = stack.CurrentNativeFunction; int paramsSize = Native_UFunction.Get_ParmsSize(function); int numParams = Native_UFunction.Get_NumParms(function); bool hasOutParams = Native_UFunction.HasAnyFunctionFlags(function, EFunctionFlags.HasOutParms); IntPtr *outParamsBufferPtr = stackalloc IntPtr[numParams]; byte * paramsBufferPtr = stackalloc byte[paramsSize]; IntPtr paramsBuffer = (IntPtr)paramsBufferPtr; // We could skip this memzero as stackalloc will (always?) zero memory even though the spec states // "The content of the newly allocated memory is undefined." // https://github.com/dotnet/coreclr/issues/1279 FMemory.Memzero(paramsBuffer, paramsSize); if (hasOutParams) { int paramIndex = 0; foreach (IntPtr param in new NativeReflection.NativeFieldIterator(Runtime.Classes.UProperty, function, false)) { // Not required but using for Debug.Assert() when getting the value stack.MostRecentPropertyAddress = IntPtr.Zero; stack.Step(stack.Object, paramsBuffer + Native_UProperty.GetOffset_ForUFunction(param)); outParamsBufferPtr[paramIndex] = stack.MostRecentPropertyAddress; if (Native_UProperty.HasAnyPropertyFlags(param, EPropertyFlags.ReturnParm)) { // This should be UObject::execEndFunctionParms which will just do "stack->Code--;" for allowing // the caller to use PFINISH aftwards outParamsBufferPtr[paramIndex] = result; } paramIndex++; } } else { foreach (IntPtr param in new NativeReflection.NativeFieldIterator(Runtime.Classes.UProperty, function, false)) { stack.Step(stack.Object, paramsBuffer + Native_UProperty.GetOffset_ForUFunction(param)); } } stack.PFinish();// Skip EX_EndFunctionParms // Call the managed function invoker which will marshal the params from the native params buffer and then call the // target managed function managedFunctionInvoker(paramsBuffer, obj); // Copy out params from the temp buffer //if (hasOutParams) // 4.20 DestructorLink change (see below) { int paramIndex = 0; foreach (IntPtr paramProp in new NativeReflection.NativeFieldIterator(Runtime.Classes.UProperty, function, false)) { EPropertyFlags paramFlags = Native_UProperty.GetPropertyFlags(paramProp); if ((paramFlags & EPropertyFlags.OutParm) == EPropertyFlags.OutParm && (paramFlags & EPropertyFlags.ConstParm) != EPropertyFlags.ConstParm) { Debug.Assert(outParamsBufferPtr[paramIndex] != IntPtr.Zero); // - See "REMOVING the DestroyValue call" below. // Destroy the existing memory (this assumed the existing memory is valid or at least memzerod) //Native_UProperty.DestroyValue(paramProp, outParamsBufferPtr[paramIndex]); // A raw memcpy should be OK since the managed invoker should have set this memory appropriately. FMemory.Memcpy(outParamsBufferPtr[paramIndex], paramsBuffer + Native_UProperty.GetOffset_ForUFunction(paramProp), Native_UProperty.Get_ElementSize(paramProp));// Should be ArrayDim*ElementSize but ArrayDim should always be 1 for params } else { // 4.20 the original DestructorLink code doesn't work as expected (did it ever work?). DestructorLink seems to only be // used on delegate functions (e.g. /Script/Engine.ActorOnClickedSignature__DelegateSignature). // - For now call destroy everything other than out params (is this safe?) // - TODO: Replace this with custom Stack.Code stepping as described above? Native_UProperty.DestroyValue_InContainer(paramProp, paramsBuffer); } paramIndex++; } } // Parameters are copied when calling stack->Step(). We are responsible for destroying non-blittable types // which were copied (FString, TArray, etc). For C++ this works out well due to the copy constructors etc. // // Example where an FString is constructed from stack->Step(): // UObject::execStringConst(...) { *(FString*)RESULT_PARAM = (ANSICHAR*)Stack.Code; } // // For C# it might be better if we reimplemented all of the IMPLEMENT_VM_FUNCTION functions to reduce the amount // of copying as we currently need ANOTHER copy to get it from the temp buffer into a C# type (which is done // inside the managedFunctionInvoker function) /*foreach (IntPtr paramProp in new NativeReflection.NativeFieldIterator(Runtime.Classes.UProperty, function, * EFieldIteratorType.Destructor, false)) * { * // When is this ever false? It seems to be checked in UObject::ProcessEvent() * // "Destroy local variables except function parameters." - used for BP locals? * Debug.Assert(Native_UProperty.IsInContainer(paramProp, paramsSize)); * * // Out params are copied to the memory maintained by the caller so only destroy "by value" parameters. * if (!Native_UProperty.HasAnyPropertyFlags(paramProp, EPropertyFlags.OutParm)) * { * Native_UProperty.DestroyValue_InContainer(paramProp, paramsBuffer); * } * }*/ }
private unsafe void InvokeFunctionImpl(IntPtr obj, IntPtr stackPtr, IntPtr result) { // TODO: Move this entire function into C++ for better performance? C++ should only need the managedFunctionInvoker // function and the rest should be do-able in C++. Use C++ anonymous function capture to hold onto managedFunctionInvoker // and then invoke it from C++. // TODO: Use ContainerVoidPtrToValuePtr(param, paramsBuffer, 0) instead of GetOffset_ForUFunction? // - ArrayDim isn't available on params so it should be safe (and maybe faster?) to use GetOffset_ForUFunction // - "Arrays aren't allowed as function parameters" - HeaderParser::GetVarNameAndDim // TODO: Inline this as generated IL code in the target invoker func without the loops and with less copying // NOTE: We shouldn't have to call prop->InitializeValue/InitializeValue_InContainer as the caller should have // already initialized param memory which we will be processing. (InitializeValue is used for structs which // have a custom default constructor which needs to be called in order to create a valid default value). // It seems non-native bools also require InitializeValue (see UBoolProperty::SetBoolSize). // Use XXXX_generated.h files as reference for checking we are doing things somewhat correctly here // ScriptCore.cpp / Stack.h / ScriptMacros.h - P_GET_XXXX / FFrame::StepXXXX mostly handle the native code path FFrame stack = new FFrame(stackPtr); UFunction.FuncInvokerManaged managedFunctionInvoker; if (managedInvokersByAddress.TryGetValue(stack.CurrentNativeFunction, out managedFunctionInvoker)) { // FirstPropertyToInit only seems to be used by BP functions (FKismetCompilerContext::SetCalculatedMetaDataAndFlags) // If this isn't the case then we need to ensure we use this to init these params by calling InitializeValue_InContainer Debug.Assert(Native_UFunction.Get_FirstPropertyToInit(stack.CurrentNativeFunction) == IntPtr.Zero); if (stack.Code != IntPtr.Zero) { // If stack->Code is set that means the function is being called by a BP function. stack->Code should hold // the VM opcodes / param memory which need to be obtained by using stack->Step() for each parameter which // also advances stack->Code HandleInvokeFunctionFromBP(obj, stack, result, managedFunctionInvoker); } else { // If stack->Code is nullptr that means this function is being called by a native code path (such as a direct // call to UObject::ProcessEvent(). We should get the parameters from stack->PropertyChainForCompiledIn // which should initially point to function->Children. HandleInvokeFunctionFromNative(obj, stack, result, managedFunctionInvoker); } } else { // The managed __Invoker function doesn't exist. This is will happen if ManagedUnrealFunctionInfo.BPImplementation // is true or CodeGeneratorSettings.UseImplicitBlueprintImplementableEvent is true with no _Implementation method. // (otherwise this is pretty bad and very silent - possibly add some additional logging if this ever happens) if (stack.Code != IntPtr.Zero) { // This will advance the VM stack->Code address and satisfy the memory for the caller Native_UObject.SkipFunction(stack.Object, stackPtr, result, stack.CurrentNativeFunction); } else { // stack->Code is null, we can't call UObject::SkipFunction (which needs stack->Code) // This will happen in cases where there is a BlueprintImplementedEvent defined in C# but there isn't // an implementation defined in blueprint. This is therefore treated as a direct native call. // TODO: ensure all memory is still freed correctly HandleInvokeFunctionFromNative(obj, stack, result, null); } } }