Skip to content

[RGen] Complete the native invoke implementation. #23049

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,114 @@ static partial class BindingSyntaxFactory {
#pragma warning restore format
}

/// <summary>
/// Returns an expression syntax representing the conversion of a native return value to its corresponding managed type
/// after a native delegate (block) invocation within a trampoline.
/// This method handles various conversions, such as:
/// - Converting a native byte (0 or 1) to a C# bool.
/// - Converting a native integer to a C# enum (both regular and smart enums from NSString).
/// - Creating a C# string from a native string handle (CFStringRef).
/// - Creating a C# string array from a native array handle (CFArrayRef of CFStringRef).
/// - Creating specific managed objects like CMSampleBuffer or AudioBuffers from their native handles.
/// - Getting an NSObject or INativeObject instance from a native handle.
/// - Getting a C# array of NSObject or INativeObject from a native array handle.
/// If no conversion is needed, it returns an identifier for the auxiliary variable holding the native return value.
/// </summary>
/// <param name="typeInfo">The <see cref="TypeInfo"/> of the delegate, used to determine the return type and its properties.</param>
/// <param name="auxVariableName">The name of the auxiliary variable holding the native return value.</param>
/// <returns>An <see cref="ExpressionSyntax"/> for the converted managed return value, or null if the delegate returns void.</returns>
internal static ExpressionSyntax? GetTrampolineNativeInvokeReturnType (TypeInfo typeInfo, string auxVariableName)
{
// ignore those types that are not delegates or that are a delegate with a void return type
if (!typeInfo.IsDelegate || typeInfo.Delegate.ReturnType.IsVoid)
return null;

var auxIdentifier = IdentifierName (auxVariableName);
#pragma warning disable format
// based on the return type of the delegate we build a statement that will return the expected value
return typeInfo.Delegate.ReturnType switch {
// auxVariable != 0
{ SpecialType: SpecialType.System_Boolean }
=> CastToBool (auxVariableName, typeInfo.Delegate.ReturnType),

// enum values

// normal enum, cast to the enum type
// (EnumType) auxVariable

{ IsEnum: true, IsNativeEnum: true } => CastNativeToEnum (auxVariableName, typeInfo.Delegate.ReturnType),

// smart enum, get type from string
{ IsEnum: true, IsSmartEnum: true, IsNativeEnum: false }
=> GetSmartEnumFromNSString (typeInfo.Delegate.ReturnType, Argument (auxIdentifier)),

// string from native handle
// CFString.FromHandle (auxVariable)!
{ SpecialType: SpecialType.System_String, IsNullable: false}
=> SuppressNullableWarning (StringFromHandle ([Argument (auxIdentifier)])),

// CFString.FromHandle (auxVariable)
{ SpecialType: SpecialType.System_String, IsNullable: true}
=> StringFromHandle ([Argument (auxIdentifier)]),

// string array
// CFArray.StringArrayFromHandle (obj)!
{ IsArray: true, ArrayElementType: SpecialType.System_String, IsNullable: false}
=> SuppressNullableWarning (StringArrayFromHandle ([Argument (auxIdentifier)])),

// CFArray.StringArrayFromHandle (obj)
{ IsArray: true, ArrayElementType: SpecialType.System_String, IsNullable: true}
=> StringArrayFromHandle ([Argument (auxIdentifier)]),

{ FullyQualifiedName: "CoreMedia.CMSampleBuffer" } =>
New (CMSampleBuffer, [Argument (auxIdentifier), BoolArgument (false)]),

// AudioToolbox.AudioBuffers
// new global::AudioToolbox.AudioBuffers ({0})
{ FullyQualifiedName: "AudioToolbox.AudioBuffers" } =>
New (AudioBuffers, [Argument (auxIdentifier), BoolArgument (false)]),

// INativeObject from a native handle
// Runtime.GetINativeObject<NSString> (auxVariable, false)!;
{ IsINativeObject: true, IsNSObject: false }
=> GetINativeObject (
nsObjectType: typeInfo.Delegate.ReturnType.ToNonNullable ().GetIdentifierSyntax (),
args: [
Argument (auxIdentifier),
BoolArgument (false)
],
suppressNullableWarning: !typeInfo.Delegate.ReturnType.IsNullable),

// NSObject from a native handle
// Runtime.GetNSObject<NSString> (auxVariable, false)!;
{ IsNSObject: true }
=> GetNSObject (
nsObjectType: typeInfo.Delegate.ReturnType.ToNonNullable ().GetIdentifierSyntax (),
args: [
Argument (auxIdentifier),
BoolArgument (false)
],
suppressNullableWarning: !typeInfo.Delegate.ReturnType.IsNullable),

// CFArray.ArrayFromHandle<global::Foundation.NSMetadataItem>
{ IsArray: true, ArrayElementTypeIsWrapped: true }
=> GetCFArrayFromHandle (typeInfo.Delegate.ReturnType.ToArrayElementType ().ToNonNullable ().GetIdentifierSyntax (), [
Argument (auxIdentifier)
], suppressNullableWarning: !typeInfo.Delegate.ReturnType.IsNullable),

// CFArray.ArrayFromHandle<global::Foundation.NSMetadataItem>
{ IsArray: true, ArrayElementIsINativeObject: true }
=> GetCFArrayFromHandle (typeInfo.Delegate.ReturnType.ToArrayElementType ().ToNonNullable ().GetIdentifierSyntax (), [
Argument (auxIdentifier)
], suppressNullableWarning: !typeInfo.Delegate.ReturnType.IsNullable),

// default case, return the value as is
_ => auxIdentifier,

};
#pragma warning restore format
}

/// <summary>
/// Returns the expression for the creation of the NativeInvocationClass for a given trampoline.
/// </summary>
Expand Down Expand Up @@ -487,6 +595,14 @@ internal static ArgumentSyntax GetTrampolineInvokeArgument (string trampolineNam
return argument;
}

/// <summary>
/// Generates any necessary pre-invocation statements for a by-ref trampoline argument.
/// This is used to handle special cases for by-ref parameters, such as creating temporary variables
/// for nullable or boolean types that require conversion before the trampoline is invoked.
/// Returns an empty array if no special handling is required.
/// </summary>
/// <param name="parameter">The delegate parameter to process for pre-invocation by-ref handling.</param>
/// <returns>An immutable array of syntax nodes representing the required pre-invocation statements for the by-ref argument.</returns>
internal static ImmutableArray<SyntaxNode> GetTrampolinePreInvokeByRefArgument (in DelegateParameter parameter)
{
// there are two cases in which we need to do something with the byref parameters:
Expand Down Expand Up @@ -584,6 +700,16 @@ internal static ImmutableArray<SyntaxNode> GetTrampolineInitializationByRefArgum
return [expr];
}

/// <summary>
/// Generates any necessary post-invocation statements for a by-ref trampoline argument.
/// This is used to handle special cases for by-ref parameters, such as assigning back values
/// from temporary variables for nullable types or converting boolean values back to their native
/// representation after the trampoline is invoked.
/// Returns an empty array if no special handling is required.
/// </summary>
/// <param name="trampolineName">The name of the trampoline. This parameter is not directly used in the current implementation but is kept for consistency with related methods.</param>
/// <param name="parameter">The delegate parameter to process for post-invocation by-ref handling.</param>
/// <returns>An immutable array of syntax nodes representing the required post-invocation statements for the by-ref argument.</returns>
internal static ImmutableArray<SyntaxNode> GetTrampolinePostInvokeByRefArgument (string trampolineName,
in DelegateParameter parameter)
{
Expand Down Expand Up @@ -955,6 +1081,12 @@ internal static MemberDeclarationSyntax GetTrampolineNativeInvokeSignature (in T
return method;
}

/// <summary>
/// Returns an array of syntax nodes representing initializations required for a 'byref' parameter before invoking the native trampoline.
/// This method handles the initialization of 'byref' parameters by assigning them their default value.
/// </summary>
/// <param name="parameter">The delegate parameter, which is expected to be 'byref'.</param>
/// <returns>An immutable array of syntax nodes representing the initialization statements for the 'byref' parameter.</returns>
internal static ImmutableArray<SyntaxNode> GetTrampolineNativeInitializationByRefArgument (in DelegateParameter parameter)
{
// create the pointer variable and assign it to its default value
Expand Down Expand Up @@ -1112,44 +1244,24 @@ internal static ImmutableArray<SyntaxNode> GetTrampolinePostNativeInvokeArgument
))
]))],

{ IsProtocol: true } => [ExpressionStatement (
KeepAlive (
// use the nomenclator to get the name for the variable type
Nomenclator.GetNameForVariableType (parameter.Name, Nomenclator.VariableType.Handle)!
))],
{ IsProtocol: true } => [ExpressionStatement (KeepAlive (parameter.Name))],

// special types

// CoreMedia.CMSampleBuffer
{ FullyQualifiedName: "CoreMedia.CMSampleBuffer" } => [ExpressionStatement (
KeepAlive (
// use the nomenclator to get the name for the variable type
Nomenclator.GetNameForVariableType (parameter.Name, Nomenclator.VariableType.Handle)!
))],
{ FullyQualifiedName: "CoreMedia.CMSampleBuffer" } => [ExpressionStatement (KeepAlive (parameter.Name))],

// AudioToolbox.AudioBuffers
{ FullyQualifiedName: "AudioToolbox.AudioBuffers" } => [ExpressionStatement (
KeepAlive (
// use the nomenclator to get the name for the variable type
Nomenclator.GetNameForVariableType (parameter.Name, Nomenclator.VariableType.Handle)!
))],
{ FullyQualifiedName: "AudioToolbox.AudioBuffers" } => [ExpressionStatement (KeepAlive (parameter.Name))],

// general NSObject/INativeObject, has to be after the special types otherwise the special types will
// fall into the NSObject/INativeObject case

// same name, native handle
{ IsNSObject: true } => [ExpressionStatement (
KeepAlive (
// use the nomenclator to get the name for the variable type
Nomenclator.GetNameForVariableType (parameter.Name, Nomenclator.VariableType.Handle)!
))],
{ IsNSObject: true } => [ExpressionStatement (KeepAlive (parameter.Name))],

// same name, native handle
{ IsINativeObject: true } => [ExpressionStatement (
KeepAlive (
// use the nomenclator to get the name for the variable type
Nomenclator.GetNameForVariableType (parameter.Name, Nomenclator.VariableType.Handle)!
))],
{ IsINativeObject: true } => [ExpressionStatement (KeepAlive (parameter.Name))],

// by default, we will use the parameter name as is and the type of the parameter
_ => [],
Expand Down Expand Up @@ -1296,14 +1408,22 @@ internal static ImmutableArray<TrampolineArgumentSyntax> GetTrampolineNativeInvo
foreach (var parameter in delegateInfo.Parameters) {
var argument = new TrampolineArgumentSyntax (GetTrampolineNativeInvokeArgument (trampolineName, parameter)) {
Initializers = GetTrampolineInvokeArgumentInitializations (trampolineName, parameter),
PreDelegateCallConversion = GetTrampolinePreInvokeArgumentConversions (trampolineName, parameter),
PostDelegateCallConversion = GetTrampolinePostInvokeArgumentConversions (trampolineName, parameter),
PreDelegateCallConversion = GetTrampolinePreNativeInvokeArgumentConversions (trampolineName, parameter),
PostDelegateCallConversion = GetTrampolinePostNativeInvokeArgumentConversions (trampolineName, parameter),
};
bucket.Add (argument);
}
return bucket.ToImmutable ();
}

/// <summary>
/// Generates the statement syntax for calling the native invoker delegate.
/// This method constructs the invocation of the delegate using the provided arguments,
/// and handles whether the delegate returns a value or is void.
/// </summary>
/// <param name="delegateInfo">The information about the delegate being called.</param>
/// <param name="argumentSyntax">The immutable array of argument syntax for the delegate call.</param>
/// <returns>A <see cref="StatementSyntax"/> representing the call to the native invoker delegate.</returns>
internal static StatementSyntax CallNativeInvokerDelegate (in DelegateInfo delegateInfo,
in ImmutableArray<TrampolineArgumentSyntax> argumentSyntax)
{
Expand Down
21 changes: 19 additions & 2 deletions src/rgen/Microsoft.Macios.Generator/Emitters/TrampolineEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public bool TryEmitNativeInvocationClass (in TypeInfo typeInfo, string trampolin
var delegateIdentifier = Nomenclator.GetTrampolineClassName (trampolineName, Nomenclator.TrampolineClassType.DelegateType);

using (var classBlock = classBuilder.CreateBlock ($"internal sealed class {className} : TrampolineBlockBase", true)) {
classBlock.WriteLine ($"{delegateName} invoker;");
classBlock.WriteLine ($"{delegateName} {Nomenclator.GetNativeInvokerVariableName ()};");
classBlock.WriteLine (); // empty line for readability
// constructor
classBlock.WriteRaw (
Expand Down Expand Up @@ -139,7 +139,24 @@ public bool TryEmitNativeInvocationClass (in TypeInfo typeInfo, string trampolin
classBlock.WriteLine (); // empty line for readability
// invoke method
using (var invokeBlock = classBlock.CreateBlock (GetTrampolineNativeInvokeSignature (typeInfo).ToString (), true)) {
invokeBlock.WriteLine ("// TODO: generate invoke method.");
// retrieve the arguments for the invoker execution.
var argumentSyntax = GetTrampolineNativeInvokeArguments (trampolineName, typeInfo.Delegate!);
// write the conversion code for the arguments
foreach (var argument in argumentSyntax) {
invokeBlock.Write (argument.PreDelegateCallConversion, verifyTrivia: false);
}

// execute the native invoker delegate
invokeBlock.WriteLine ($"{CallNativeInvokerDelegate (typeInfo.Delegate!, argumentSyntax)}");

// build any needed post conversion operations after calling the delegate
foreach (var argument in argumentSyntax) {
invokeBlock.Write (argument.PostDelegateCallConversion, verifyTrivia: false);
}

// perform any return conversions needed
if (typeInfo.Delegate!.ReturnType.SpecialType != SpecialType.System_Void)
invokeBlock.WriteLine ($"return {GetTrampolineNativeInvokeReturnType (typeInfo, Nomenclator.GetReturnVariableName ())};");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ public unsafe NIDTrampolinePropertyTests_CreateObject (global::ObjCRuntime.Block

unsafe global::Foundation.NSObject Invoke (global::Foundation.NSObject obj)
{
// TODO: generate invoke method.
if (obj is null)
global::ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (obj));
var obj__handle__ = obj.GetHandle ();
var ret = invoker (BlockLiteral, obj__handle__);
global::System.GC.KeepAlive (obj);
return global::ObjCRuntime.Runtime.GetNSObject<global::Foundation.NSObject> (ret, false)!;
}
}

Expand Down Expand Up @@ -128,7 +133,7 @@ public unsafe NIDAction (global::ObjCRuntime.BlockLiteral *block) : base (block)

unsafe void Invoke ()
{
// TODO: generate invoke method.
invoker (BlockLiteral);
}
}

Expand Down Expand Up @@ -187,7 +192,8 @@ public unsafe NIDCIKernelRoiCallback (global::ObjCRuntime.BlockLiteral *block) :

unsafe global::CoreGraphics.CGRect Invoke (int index, global::CoreGraphics.CGRect rect)
{
// TODO: generate invoke method.
var ret = invoker (BlockLiteral, index, rect);
return ret;
}
}

Expand Down Expand Up @@ -245,7 +251,11 @@ public unsafe NIDActionArity1string (global::ObjCRuntime.BlockLiteral *block) :

unsafe void Invoke (string obj)
{
// TODO: generate invoke method.
if (obj is null)
global::ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (obj));
var nsobj = global::CoreFoundation.CFString.CreateNative (obj);
invoker (BlockLiteral, nsobj);
global::CoreFoundation.CFString.ReleaseNative (nsobj);
}
}

Expand Down Expand Up @@ -303,7 +313,7 @@ public unsafe NIDActionArity1int (global::ObjCRuntime.BlockLiteral *block) : bas

unsafe void Invoke (int obj)
{
// TODO: generate invoke method.
invoker (BlockLiteral, obj);
}
}

Expand Down Expand Up @@ -361,7 +371,7 @@ public unsafe NIDActionArity1bool (global::ObjCRuntime.BlockLiteral *block) : ba

unsafe void Invoke (bool obj)
{
// TODO: generate invoke method.
invoker (BlockLiteral, obj ? (byte) 1 : (byte) 0);
}
}

Expand Down Expand Up @@ -419,7 +429,15 @@ public unsafe NIDAVAssetImageGenerateAsynchronouslyForTimeCompletionHandler (glo

unsafe void Invoke (global::CoreGraphics.CGImage imageRef, global::CoreMedia.CMTime actualTime, global::Foundation.NSError error)
{
// TODO: generate invoke method.
if (imageRef is null)
global::ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (imageRef));
var imageRef__handle__ = imageRef.GetHandle ();
if (error is null)
global::ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (error));
var error__handle__ = error.GetHandle ();
invoker (BlockLiteral, imageRef__handle__, actualTime, error__handle__);
global::System.GC.KeepAlive (imageRef);
global::System.GC.KeepAlive (error);
}
}

Expand Down Expand Up @@ -479,7 +497,12 @@ public unsafe NIDAVAudioEngineManualRenderingBlock (global::ObjCRuntime.BlockLit

unsafe global::AVFoundation.AVAudioEngineManualRenderingStatus Invoke (uint numberOfFrames, global::AudioToolbox.AudioBuffers outBuffer, ref int outError)
{
// TODO: generate invoke method.
if (outBuffer is null)
global::ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (outBuffer));
var outBuffer__handle__ = outBuffer.GetHandle ();
var ret = invoker (BlockLiteral, numberOfFrames, outBuffer__handle__, (int*) global::System.Runtime.CompilerServices.Unsafe.AsPointer<int> (ref outError));
global::System.GC.KeepAlive (outBuffer);
return (global::AVFoundation.AVAudioEngineManualRenderingStatus) (long) ret;
}
}

Expand Down
Loading
Loading