CsWinRT icon indicating copy to clipboard operation
CsWinRT copied to clipboard

Vftbl delegate codegen strategy is not compatible with NativeAOT

Open hez2010 opened this issue 3 years ago • 5 comments
trafficstars

Describe the bug CsWinRT uses runtime dynamic il emit to generate delegate types which is not supported by NativeAOT: https://github.com/microsoft/CsWinRT/search?q=GetDelegateType

For example, CsWinRT produces this code for ABI.Windows.Foundation.IAsyncOperation<TResult>.Vftbl:

	unsafe static Vftbl()
	{
		PIID = GuidGenerator.CreateIID(typeof(IAsyncOperation<TResult>));
		GetResults_2_Type = Expression.GetDelegateType(typeof(void*), Marshaler<TResult>.AbiType.MakeByRefType(), typeof(int));
		DelegateCache = new Delegate[3];
		AbiToProjectionVftable = new Vftbl
		{
			IInspectableVftbl = IInspectable.Vftbl.AbiToProjectionVftable,
			_put_Completed_0 = (void*)Marshal.GetFunctionPointerForDelegate(DelegateCache[0] = new IAsyncOperation_Delegates.put_Completed_0(Do_Abi_put_Completed_0)),
			_get_Completed_1 = (void*)Marshal.GetFunctionPointerForDelegate(DelegateCache[1] = new IAsyncOperation_Delegates.get_Completed_1(Do_Abi_get_Completed_1)),
			GetResults_2 = Delegate.CreateDelegate(GetResults_2_Type, typeof(Vftbl)!.GetMethod("Do_Abi_GetResults_2", BindingFlags.Static | BindingFlags.NonPublic)!.MakeGenericMethod(Marshaler<TResult>.AbiType))
		};
		IntPtr* nativeVftbl = (IntPtr*)ComWrappersSupport.AllocateVtableMemory(typeof(Vftbl), Marshal.SizeOf<IInspectable.Vftbl>() + sizeof(IntPtr) * 3);
		Marshal.StructureToPtr(AbiToProjectionVftable.IInspectableVftbl, (nint)nativeVftbl, fDeleteOld: false);
		nativeVftbl[6] = (nint)AbiToProjectionVftable._put_Completed_0;
		nativeVftbl[7] = (nint)AbiToProjectionVftable._get_Completed_1;
		nativeVftbl[8] = Marshal.GetFunctionPointerForDelegate(AbiToProjectionVftable.GetResults_2);
		AbiToProjectionVftablePtr = (nint)nativeVftbl;
	}

	private unsafe static int Do_Abi_GetResults_2<TResultAbi>(void* thisPtr, out TResultAbi __return_value__)
	{
		TResult ____return_value__ = default(TResult);
		__return_value__ = default(TResultAbi);
		try
		{
			____return_value__ = ComWrappersSupport.FindObject<global::Windows.Foundation.IAsyncOperation<TResult>>(new IntPtr(thisPtr)).GetResults();
			__return_value__ = (TResultAbi)Marshaler<TResult>.FromManaged(____return_value__);
		}
		catch (Exception ex)
		{
			ExceptionHelpers.SetErrorInfo(ex);
			return ExceptionHelpers.GetHRForException(ex);
		}
		return 0;
	}

where Expression.GetDelegateType(typeof(void*), Marshaler<TResult>.AbiType.MakeByRefType(), typeof(int)) is completely not supported by NativeAOT because it is emitting a brand new type at runtime.

Instead of emitting delegate type at runtime, the codegen strategy should be source generating a delegate declaration:

+       delegate int Do_Abi_GetResults_2_Delegate<TResultAbi>(void* thisPtr, out TResultAbi __return_value__);

	unsafe static Vftbl()
	{
		PIID = GuidGenerator.CreateIID(typeof(IAsyncOperation<TResult>));
-		GetResults_2_Type = Expression.GetDelegateType(typeof(void*), Marshaler<TResult>.AbiType.MakeByRefType(), typeof(int));
+		GetResults_2_Type = typeof(Do_Abi_GetResults_2_Delegate<>).MakeGenericType(Marshaler<TResult>.AbiType);
		DelegateCache = new Delegate[3];
		AbiToProjectionVftable = new Vftbl
		{
			IInspectableVftbl = IInspectable.Vftbl.AbiToProjectionVftable,
			_put_Completed_0 = (void*)Marshal.GetFunctionPointerForDelegate(DelegateCache[0] = new IAsyncOperation_Delegates.put_Completed_0(Do_Abi_put_Completed_0)),
			_get_Completed_1 = (void*)Marshal.GetFunctionPointerForDelegate(DelegateCache[1] = new IAsyncOperation_Delegates.get_Completed_1(Do_Abi_get_Completed_1)),
			GetResults_2 = Delegate.CreateDelegate(GetResults_2_Type, typeof(Vftbl)!.GetMethod("Do_Abi_GetResults_2", BindingFlags.Static | BindingFlags.NonPublic)!.MakeGenericMethod(Marshaler<TResult>.AbiType))
		};
		IntPtr* nativeVftbl = (IntPtr*)ComWrappersSupport.AllocateVtableMemory(typeof(Vftbl), Marshal.SizeOf<IInspectable.Vftbl>() + sizeof(IntPtr) * 3);
		Marshal.StructureToPtr(AbiToProjectionVftable.IInspectableVftbl, (nint)nativeVftbl, fDeleteOld: false);
		nativeVftbl[6] = (nint)AbiToProjectionVftable._put_Completed_0;
		nativeVftbl[7] = (nint)AbiToProjectionVftable._get_Completed_1;
		nativeVftbl[8] = Marshal.GetFunctionPointerForDelegate(AbiToProjectionVftable.GetResults_2);
		AbiToProjectionVftablePtr = (nint)nativeVftbl;
	}

	private unsafe static int Do_Abi_GetResults_2<TResultAbi>(void* thisPtr, out TResultAbi __return_value__)
	{
		TResult ____return_value__ = default(TResult);
		__return_value__ = default(TResultAbi);
		try
		{
			____return_value__ = ComWrappersSupport.FindObject<global::Windows.Foundation.IAsyncOperation<TResult>>(new IntPtr(thisPtr)).GetResults();
			__return_value__ = (TResultAbi)Marshaler<TResult>.FromManaged(____return_value__);
		}
		catch (Exception ex)
		{
			ExceptionHelpers.SetErrorInfo(ex);
			return ExceptionHelpers.GetHRForException(ex);
		}
		return 0;
	}

This won't bring any breaking change, please fix it so that we can use CsWinRT with NativeAOT.

Version Info

CsWinRT 2.0

/cc: @manodasanW

hez2010 avatar Aug 17 '22 12:08 hez2010

typeof(Do_Abi_GetResults_2_Delegate<>).MakeGenericType(Marshaler<TResult>.AbiType);

MakeGenericType is not really compatible with NativeAOT either.

jkotas avatar Aug 17 '22 13:08 jkotas

Yes, but at least it works against shared generics, and can workaround with rd.xml.

hez2010 avatar Aug 17 '22 13:08 hez2010

Thanks @hez2010 for bringing this up. We do need to source generate the delegate to make it AOT compatible. With that said, the issue with the proposed approach and why we were using Expression.GetDelegateType is that delegates declared within generic interfaces or delegates using generics cannot be passed to GetFunctionPointerForDelegate.

I have been working on an alternative approach as seen here where in the C#/WinRT tool when we encounter any generic instantiation like IList<int> in parameters, return types, properties and so on, it will also code generate the delegates needed by IList for that specific generic and register it for it to be looked up later. I was initially working on it to address another issue, but it will also end up solving this issue too.

There are still some issues with that change that caused it to not make 2.0 such as issues with object parameters and boxed types where it can't tell there was going to be a IList<int> or int[] passed but at runtime we still generate the IList vftbl which will need the delegates and with the change in the current state probably not being trimming friendly due to the registration in the dictionary for lookups which will probably make it seem like more types are used than there is and end up not trimming them. I plan on continuing to work on addressing those issues and get it in for an upcoming build.

manodasanW avatar Aug 18 '22 00:08 manodasanW

Any timeline on this @manodasanW ?

charlesroddie avatar Oct 10 '22 15:10 charlesroddie

@charlesroddie this is being worked on along with a couple other related things we discovered as part of an exercise we did where we made manual changes to the generated projections and tried to get our functional tests passing with AOT. I don't have an exact timeline to share at the moment, but I will keep this thread posted as we make progress.

manodasanW avatar Oct 19 '22 00:10 manodasanW