Fixes #22219.
I decided to try simplifying the code when bringing this feature over from .NET Framework. The .NETFX x86 implementation of this marshaler adds a lot of extra code-paths, and intercepts the calling stub to enable it to exactly replicate the behavior of what would be the native code by copy-constructing an object in-place where it goes on the stack for the native call.
Instead of adding all of that extra goo, I decided to keep the implementation much more similar to the non-x86 implementation from .NETFX. Instead of intercepting the call and adding bookkeeping helper stubs, the marshaler just copies to a local in the IL stub, just like non-x86. When calling the native function, it just loads the local onto the IL stack and calls the native function as a normal function. There is a difference there, but I cannot think of a way that the difference is observable to the user.
The non-x86 implementation is identical to the .NETFX implementation.
IDS_EE_BADMARSHAL_DECIMALARRAY "Invalid managed/unmanaged type combination (Decimal[] must be paired with an ArraySubType of Struct or Currency)."
IDS_EE_BADMARSHAL_WINRT_MARSHAL_AS "Invalid managed/unmanaged type combination (Windows Runtime parameters and fields must not have a MarshalAs attribute set)."
IDS_EE_BADMARSHAL_WINRT_MISSING_GUID "Invalid managed/unmanaged type combination (Windows Runtime interfaces, classes and delegates must have a Guid or a default interface)."
+ IDS_EE_BADMARSHAL_WINRT_COPYCTOR "Windows Runtime marshaler does not support types with copy constructor."
IDS_EE_BADMARSHAL_WINRT_DELEGATE "Invalid managed/unmanaged type combination (Delegates must be Windows Runtime delegates)."
IDS_EE_BADMARSHAL_DEFAULTIFACE_NOT_WINRT_IFACE "The default interface must refer to a Windows Runtime interface with a GUID."
IDS_EE_BADMARSHAL_DEFAULTIFACE_NOT_SUBTYPE "The default interface must refer to an interface that is implemented by the type."
IDS_EE_BADMARSHAL_ASANYRESTRICTION "AsAny cannot be used on return types, ByRef parameters, ArrayWithOffset, or parameters passed from unmanaged to managed."
IDS_EE_BADMARSHAL_VBBYVALSTRRESTRICTION "VBByRefStr can only be used in combination with in/out, ByRef managed-to-unmanaged strings."
IDS_EE_BADMARSHAL_AWORESTRICTION "ArrayWithOffsets can only be marshaled as inout, non-ByRef, managed-to-unmanaged parameters."
+ IDS_EE_BADMARSHAL_COPYCTORRESTRICTION "Classes with copy-ctors can only be marshaled by value."
IDS_EE_BADMARSHAL_ARGITERATORRESTRICTION "ArgIterators cannot be marshaled ByRef."
IDS_EE_BADMARSHAL_HANDLEREFRESTRICTION "HandleRefs cannot be marshaled ByRef or from unmanaged to managed."
IDS_EE_BADMARSHAL_SAFEHANDLENATIVETOCOM "SafeHandles cannot be marshaled from unmanaged to managed."
#define IDS_EE_NDIRECT_GETPROCADDR_WIN_DLL 0x2644
#define IDS_EE_NDIRECT_GETPROCADDR_UNIX_SO 0x2645
#define IDS_EE_BADMARSHAL_STRING_OUT 0x2646
+#define IDS_EE_BADMARSHAL_COPYCTORRESTRICTION 0x2647
+#define IDS_EE_BADMARSHAL_WINRT_COPYCTOR 0x2648
COMPlusThrow(kMarshalDirectiveException, IDS_EE_NDIRECT_BADNATL_THISCALL);
}
+ fHasCopyCtorArgs = info.GetMarshalType() == MarshalInfo::MARSHAL_TYPE_BLITTABLEVALUECLASSWITHCOPYCTOR ? TRUE : FALSE;
argidx++;
}
return OVERRIDDEN;
} // ILCriticalHandleMarshaler::ReturnOverride
+MarshalerOverrideStatus ILBlittableValueClassWithCopyCtorMarshaler::ArgumentOverride(NDirectStubLinker* psl,
+ BOOL byref,
+ BOOL fin,
+ BOOL fout,
+ BOOL fManagedToNative,
+ OverrideProcArgs* pargs,
+ UINT* pResID,
+ UINT argidx,
+ UINT nativeStackOffset)
+{
+ CONTRACTL
+ {
+ THROWS;
+ GC_TRIGGERS;
+ MODE_ANY;
+ }
+ CONTRACTL_END;
+
+ ILCodeStream* pslIL = psl->GetMarshalCodeStream();
+ ILCodeStream* pslILDispatch = psl->GetDispatchCodeStream();
+
+ if (byref)
+ {
+ *pResID = IDS_EE_BADMARSHAL_COPYCTORRESTRICTION;
+ return DISALLOWED;
+ }
+
+ if (fManagedToNative)
+ {
+ // 1) create new native value type local
+ // 2) run new->CopyCtor(old)
+ // 3) run old->Dtor()
+
+ LocalDesc locDesc(pargs->mm.m_pMT);
+
+ DWORD dwNewValueTypeLocal;
+
+ // Step 1
+ dwNewValueTypeLocal = pslIL->NewLocal(locDesc);
+
+ // Step 2
+ if (pargs->mm.m_pCopyCtor)
+ {
+ // Managed copy constructor has signature of CopyCtor(T* new, T old);
+ pslIL->EmitLDLOCA(dwNewValueTypeLocal);
+ pslIL->EmitLDARG(argidx);
+ pslIL->EmitCALL(pslIL->GetToken(pargs->mm.m_pCopyCtor), 2, 0);
+ }
+ else
+ {
+ pslIL->EmitLDARG(argidx);
+ pslIL->EmitLDOBJ(pslIL->GetToken(pargs->mm.m_pMT));
+ pslIL->EmitSTLOC(dwNewValueTypeLocal);
+ }
+
+ // Step 3
+ if (pargs->mm.m_pDtor)
+ {
+ // Managed destructor has signature of Destructor(T old);
+ pslIL->EmitLDARG(argidx);
+ pslIL->EmitCALL(pslIL->GetToken(pargs->mm.m_pDtor), 1, 0);
+ }
+#ifdef _TARGET_X86_
+ pslIL->SetStubTargetArgType(&locDesc); // native type is the value type
+ pslILDispatch->EmitLDLOC(dwNewValueTypeLocal); // we load the local directly
+#else
+ pslIL->SetStubTargetArgType(ELEMENT_TYPE_I); // native type is a pointer
+ pslILDispatch->EmitLDLOCA(dwNewValueTypeLocal);
+#endif
+
+ return OVERRIDDEN;
+ }
+ else
+ {
+ // nothing to do but pass the value along
+ // note that on x86 the argument comes by-value but is converted to pointer by the UM thunk
+ // so that we don't make copies that would not be accounted for by copy ctors
+ LocalDesc locDesc(pargs->mm.m_pMT);
+ locDesc.MakeCopyConstructedPointer();
+
+ pslIL->SetStubTargetArgType(&locDesc); // native type is a pointer
+ pslILDispatch->EmitLDARG(argidx);
+
+ return OVERRIDDEN;
+ }
+}
LocalDesc ILArgIteratorMarshaler::GetNativeType()
{
virtual void EmitConvertContentsNativeToCLR(ILCodeStream* pslILEmit);
};
+class ILBlittableValueClassWithCopyCtorMarshaler : public ILMarshaler
+{
+public:
+ enum
+ {
+ c_fInOnly = TRUE,
+ c_nativeSize = VARIABLESIZE,
+ c_CLRSize = sizeof(OBJECTREF),
+ };
+ LocalDesc GetManagedType()
+ {
+ LIMITED_METHOD_CONTRACT;
+ return LocalDesc();
+ }
+ LocalDesc GetNativeType()
+ {
+ LIMITED_METHOD_CONTRACT;
+ return LocalDesc();
+ }
+
+ static MarshalerOverrideStatus ArgumentOverride(NDirectStubLinker* psl,
+ BOOL byref,
+ BOOL fin,
+ BOOL fout,
+ BOOL fManagedToNative,
+ OverrideProcArgs* pargs,
+ UINT* pResID,
+ UINT argidx,
+ UINT nativeStackOffset);
+
+
+};
class ILArgIteratorMarshaler : public ILMarshaler
{
CorNativeType nativeType = NATIVE_TYPE_DEFAULT;
Assembly *pAssembly = pModule->GetAssembly();
+ BOOL fNeedsCopyCtor = FALSE;
m_BestFit = BestFit;
m_ThrowOnUnmappableChar = ThrowOnUnmappableChar;
m_ms = ms;
IfFailGoto(sig.GetElemType(NULL), lFail);
mtype = sig.PeekElemTypeNormalized(pModule, pTypeContext);
+ // Check for Copy Constructor Modifier - peek closed elem type here to prevent ELEMENT_TYPE_VALUETYPE
+ // turning into a primitive.
+ if (sig.PeekElemTypeClosed(pModule, pTypeContext) == ELEMENT_TYPE_VALUETYPE)
+ {
+ // Skip ET_BYREF
+ IfFailGoto(sigtmp.GetByte(NULL), lFail);
+
+ if (sigtmp.HasCustomModifier(pModule, "Microsoft.VisualC.NeedsCopyConstructorModifier", ELEMENT_TYPE_CMOD_REQD) ||
+ sigtmp.HasCustomModifier(pModule, "System.Runtime.CompilerServices.IsCopyConstructed", ELEMENT_TYPE_CMOD_REQD) )
+ {
+ mtype = ELEMENT_TYPE_VALUETYPE;
+ fNeedsCopyCtor = TRUE;
+ m_byref = FALSE;
+ }
+ }
}
else
{
IfFailGoto(E_FAIL, lFail);
}
+ // Check for Copy Constructor Modifier
+ if (sigtmp.HasCustomModifier(pModule, "Microsoft.VisualC.NeedsCopyConstructorModifier", ELEMENT_TYPE_CMOD_REQD) ||
+ sigtmp.HasCustomModifier(pModule, "System.Runtime.CompilerServices.IsCopyConstructed", ELEMENT_TYPE_CMOD_REQD) )
+ {
+ mtype = mtype2;
+
+ // Keep the sig pointer in sync with mtype (skip ELEMENT_TYPE_PTR) because for the rest
+ // of this method we are pretending that the parameter is a value type passed by-value.
+ IfFailGoto(sig.GetElemType(NULL), lFail);
+
+ fNeedsCopyCtor = TRUE;
+ m_byref = FALSE;
+ }
}
}
else
}
else
{
+ if (fNeedsCopyCtor)
+ {
+#ifdef FEATURE_COMINTEROP
+ if (m_ms == MARSHAL_SCENARIO_WINRT)
+ {
+ // our WinRT-optimized GetCOMIPFromRCW helpers don't support copy
+ // constructor stubs so make sure that this marshaler will not be used
+ m_resID = IDS_EE_BADMARSHAL_WINRT_COPYCTOR;
+ IfFailGoto(E_FAIL, lFail);
+ }
+#endif
+
+ MethodDesc *pCopyCtor;
+ MethodDesc *pDtor;
+ FindCopyCtor(pModule, m_pMT, &pCopyCtor);
+ FindDtor(pModule, m_pMT, &pDtor);
+
+ m_args.mm.m_pMT = m_pMT;
+ m_args.mm.m_pCopyCtor = pCopyCtor;
+ m_args.mm.m_pDtor = pDtor;
+ m_type = MARSHAL_TYPE_BLITTABLEVALUECLASSWITHCOPYCTOR;
+ }
+ else
#ifdef _TARGET_X86_
// JIT64 is not aware of normalized value types and this optimization
// (returning small value types by value in registers) is already done in JIT64.
{
case MARSHAL_TYPE_BLITTABLEVALUECLASS:
case MARSHAL_TYPE_VALUECLASS:
+ case MARSHAL_TYPE_BLITTABLEVALUECLASSWITHCOPYCTOR:
return (UINT16) m_pMT->GetNativeSize();
default:
case MARSHAL_TYPE_BLITTABLEVALUECLASS:
case MARSHAL_TYPE_BLITTABLEPTR:
case MARSHAL_TYPE_LAYOUTCLASSPTR:
+ case MARSHAL_TYPE_BLITTABLEVALUECLASSWITHCOPYCTOR:
pDispParamMarshaler = new DispParamRecordMarshaler(m_pMT);
break;
case MARSHAL_TYPE_ARGITERATOR:
strRetVal = W("ArgIterator");
break;
+ case MARSHAL_TYPE_BLITTABLEVALUECLASSWITHCOPYCTOR:
+ strRetVal = W("blittable value class with copy constructor");
+ break;
#ifdef FEATURE_COMINTEROP
case MARSHAL_TYPE_OBJECT:
strRetVal = W("VARIANT");
DEFINE_MARSHALER_TYPE(MARSHAL_TYPE_REFERENCECUSTOMMARSHALER, ReferenceCustomMarshaler, false)
DEFINE_MARSHALER_TYPE(MARSHAL_TYPE_ARGITERATOR, ArgIteratorMarshaler, false)
-
+DEFINE_MARSHALER_TYPE(MARSHAL_TYPE_BLITTABLEVALUECLASSWITHCOPYCTOR, BlittableValueClassWithCopyCtorMarshaler, false)
#ifdef FEATURE_COMINTEROP
DEFINE_MARSHALER_TYPE(MARSHAL_TYPE_OBJECT, ObjectMarshaler, false)
if(NOT CLR_CMAKE_PLATFORM_ARCH_ARM64)
add_subdirectory(IJW/ManagedCallingNative/IjwNativeDll)
add_subdirectory(IJW/NativeCallingManaged/IjwNativeCallingManagedDll)
+ add_subdirectory(IJW/CopyConstructorMarshaler)
endif()
endif(WIN32)
--- /dev/null
+cmake_minimum_required (VERSION 2.6)
+project (CopyConstructorMarshaler)
+include_directories( ${INC_PLATFORM_DIR} )
+set(SOURCES IjwCopyConstructorMarshaler.cpp)
+
+if (WIN32)
+ # 4365 - signed/unsigned mismatch
+ add_compile_options(/wd4365)
+
+ # IJW
+ add_compile_options(/clr)
+
+ # IJW requires the CRT as a dll, not linked in
+ add_compile_options(/MD$<$<OR:$<CONFIG:Debug>,$<CONFIG:Checked>>:d>)
+
+ # CMake enables /RTC1 and /EHsc by default, but they're not compatible with /clr, so remove them
+ if(CMAKE_CXX_FLAGS_DEBUG MATCHES "/RTC1")
+ string(REPLACE "/RTC1" " " CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")
+ endif()
+
+ if(CMAKE_CXX_FLAGS MATCHES "/EHsc")
+ string(REPLACE "/EHsc" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+ endif()
+
+ # IJW isn't compatible with CFG
+ if(CMAKE_CXX_FLAGS MATCHES "/guard:cf")
+ string(REPLACE "/guard:cf" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+ endif()
+
+ # IJW isn't compatible with GR-
+ if(CMAKE_CXX_FLAGS MATCHES "/GR-")
+ string(REPLACE "/GR-" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+ endif()
+
+endif()
+
+# add the shared library
+add_library (IjwCopyConstructorMarshaler SHARED ${SOURCES})
+target_link_libraries(IjwCopyConstructorMarshaler ${LINK_LIBRARIES_ADDITIONAL})
+
+# add the install targets
+install (TARGETS IjwCopyConstructorMarshaler DESTINATION bin)
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using TestLibrary;
+
+namespace CopyConstructorMarshaler
+{
+ class CopyConstructorMarshaler
+ {
+ static int Main(string[] args)
+ {
+ if(Environment.OSVersion.Platform != PlatformID.Win32NT || TestLibrary.Utilities.IsWindows7)
+ {
+ return 100;
+ }
+
+ try
+ {
+ // Load a fake mscoree.dll to avoid starting desktop
+ LoadLibraryEx(Path.Combine(Environment.CurrentDirectory, "mscoree.dll"), IntPtr.Zero, 0);
+
+ Assembly ijwNativeDll = Assembly.Load("IjwCopyConstructorMarshaler");
+ Type testType = ijwNativeDll.GetType("TestClass");
+ object testInstance = Activator.CreateInstance(testType);
+ MethodInfo testMethod = testType.GetMethod("PInvokeNumCopies");
+
+ // PInvoke will copy twice. Once from argument to parameter, and once from the managed to native parameter.
+ Assert.AreEqual(2, (int)testMethod.Invoke(testInstance, null));
+
+ testMethod = testType.GetMethod("ReversePInvokeNumCopies");
+
+ // Reverse PInvoke will copy 3 times. Two are from the same paths as the PInvoke,
+ // and the third is from the reverse P/Invoke call.
+ Assert.AreEqual(3, (int)testMethod.Invoke(testInstance, null));
+
+ testMethod = testType.GetMethod("PInvokeNumCopiesDerivedType");
+
+ // PInvoke will copy twice. Once from argument to parameter, and once from the managed to native parameter.
+ Assert.AreEqual(2, (int)testMethod.Invoke(testInstance, null));
+
+ testMethod = testType.GetMethod("ReversePInvokeNumCopiesDerivedType");
+
+ // Reverse PInvoke will copy 3 times. Two are from the same paths as the PInvoke,
+ // and the third is from the reverse P/Invoke call.
+ Assert.AreEqual(3, (int)testMethod.Invoke(testInstance, null));
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ return 101;
+ }
+ return 100;
+ }
+
+ [DllImport("kernel32.dll")]
+ static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hReservedNull, int dwFlags);
+
+ [DllImport("kernel32.dll")]
+ static extern IntPtr GetModuleHandle(string lpModuleName);
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Interop.settings.targets))\Interop.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <AssemblyName>CopyConstructorMarshaler</AssemblyName>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{49D1D482-E783-4CA9-B6BA-A9714BF81036}</ProjectGuid>
+ <OutputType>Exe</OutputType>
+ <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
+
+ <!-- IJW is Windows-only -->
+ <!-- Test unsupported outside of windows -->
+ <TestUnsupportedOutsideWindows>true</TestUnsupportedOutsideWindows>
+ <DisableProjectBuild Condition="'$(TargetsUnix)' == 'true'">true</DisableProjectBuild>
+
+ <!-- IJW is not supported on ARM64 -->
+ <DisableProjectBuild Condition="'$(Platform)' == 'arm64'">true</DisableProjectBuild>
+ </PropertyGroup>
+ <!-- Default configurations to help VS understand the configurations -->
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
+ </PropertyGroup>
+ <PropertyGroup>
+ <CopyDebugCRTDllsToOutputDirectory>true</CopyDebugCRTDllsToOutputDirectory>
+ </PropertyGroup>
+ <ItemGroup>
+ <CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
+ <Visible>False</Visible>
+ </CodeAnalysisDependentAssemblyPaths>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="CopyConstructorMarshaler.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="CMakeLists.txt" />
+ <ProjectReference Include="../FakeMscoree/CMakeLists.txt" />
+ </ItemGroup>
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
+</Project>
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#pragma managed
+class A
+{
+ int copyCount;
+
+public:
+ A()
+ :copyCount(0)
+ {};
+
+ A(A& other)
+ :copyCount(other.copyCount + 1)
+ {}
+
+ int GetCopyCount()
+ {
+ return copyCount;
+ }
+};
+
+class B : public A
+{
+ int bCopyCount;
+
+public:
+ B()
+ :A(),
+ bCopyCount(0)
+ {};
+
+ B(B& other)
+ :A(other),
+ bCopyCount(other.bCopyCount + 1)
+ {}
+
+ int GetBCopyCount()
+ {
+ return bCopyCount;
+ }
+};
+
+int Managed_GetCopyCount(A a)
+{
+ return a.GetCopyCount();
+}
+
+int Managed_GetCopyCount(B b)
+{
+ return b.GetBCopyCount();
+}
+
+#pragma unmanaged
+
+int GetCopyCount(A a)
+{
+ return a.GetCopyCount();
+}
+
+int GetCopyCount_ViaManaged(A a)
+{
+ return Managed_GetCopyCount(a);
+}
+
+int GetCopyCount(B b)
+{
+ return b.GetBCopyCount();
+}
+
+int GetCopyCount_ViaManaged(B b)
+{
+ return Managed_GetCopyCount(b);
+}
+
+#pragma managed
+
+public ref class TestClass
+{
+public:
+ int PInvokeNumCopies()
+ {
+ A a;
+ return GetCopyCount(a);
+ }
+
+ int PInvokeNumCopiesDerivedType()
+ {
+ B b;
+ return GetCopyCount(b);
+ }
+
+ int ReversePInvokeNumCopies()
+ {
+ A a;
+ return GetCopyCount_ViaManaged(a);
+ }
+
+ int ReversePInvokeNumCopiesDerivedType()
+ {
+ B b;
+ return GetCopyCount_ViaManaged(b);
+ }
+};