Skip to content
Open
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
55 changes: 52 additions & 3 deletions src/bgen/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1603,6 +1603,43 @@ void GenerateTrampolines ()
}

Dictionary<Type, bool> generated_trampolines = new Dictionary<Type, bool> ();
bool generatedNativeObjectActionInvoker;

bool CanShareNativeObjectActionInvoker (MethodInfo method, ParameterInfo [] parameters)
{
if (method.ReturnType != TypeCache.System_Void || parameters.Length != 1)
return false;

var parameter = parameters [0];
var type = parameter.ParameterType;
return !type.IsByRef && !type.IsValueType && !HasBindAsAttribute (parameter) &&
(TypeManager.IsWrappedType (type) || TypeCache.INativeObject.IsAssignableFrom (type));
Comment on lines +1615 to +1616
}

void GenerateNativeObjectActionInvoker ()
{
print ("");
print ("[UnmanagedFunctionPointerAttribute (CallingConvention.Cdecl)]");
print ($"unsafe delegate void NativeObjectAction (IntPtr block, {NativeHandleType} obj);");
print ("");
print ("internal class NativeObjectActionInvoker : TrampolineBlockBase {"); indent++;
print ("readonly NativeObjectAction invoker;");
print ("");
print_generated_code ();
print ("public unsafe NativeObjectActionInvoker (BlockLiteral *block) : base (block)");
print ("{"); indent++;
print ("invoker = block->GetDelegateForBlock<NativeObjectAction> ();");
indent--; print ("}");
print ("");
print_generated_code ();
print ("protected void Invoke (INativeObject? obj)");
print ("{"); indent++;
print ("var handle = obj.GetHandle ();");
print ("invoker (BlockPointer, handle);");
print ("GC.KeepAlive (obj);");
indent--; print ("}");
indent--; print ("}");
}

void GenerateTrampolinesForQueue (TrampolineInfo [] queue)
{
Expand Down Expand Up @@ -1688,16 +1725,23 @@ void GenerateTrampolinesForQueue (TrampolineInfo [] queue)
//
// Now generate the class that allows us to invoke a Objective-C block from C#
//
var shareNativeObjectActionInvoker = CanShareNativeObjectActionInvoker (mi, parameters);
if (shareNativeObjectActionInvoker && !generatedNativeObjectActionInvoker) {
GenerateNativeObjectActionInvoker ();
generatedNativeObjectActionInvoker = true;
}
print ("");
PrintExperimentalAttribute (ti.Type);
print ($"internal sealed class {ti.NativeInvokerName} : TrampolineBlockBase {{");
print ($"internal sealed class {ti.NativeInvokerName} : {(shareNativeObjectActionInvoker ? "NativeObjectActionInvoker" : "TrampolineBlockBase")} {{");
indent++;
print ("{0} invoker;", ti.DelegateName);
if (!shareNativeObjectActionInvoker)
print ("{0} invoker;", ti.DelegateName);
print ("");
print_generated_code ();
print ("public unsafe {0} (BlockLiteral *block) : base (block)", ti.NativeInvokerName);
print ("{"); indent++;
print ("invoker = block->GetDelegateForBlock<{0}> ();", ti.DelegateName);
if (!shareNativeObjectActionInvoker)
print ("invoker = block->GetDelegateForBlock<{0}> ();", ti.DelegateName);
indent--; print ("}");
print ("");
print ("[DynamicDependency (nameof (Create))]");
Expand All @@ -1713,6 +1757,11 @@ void GenerateTrampolinesForQueue (TrampolineInfo [] queue)
print ($"var del = ({ti.UserDelegate}?) GetExistingManagedDelegate (block);");
print ($"return del ?? new {ti.NativeInvokerName} ((BlockLiteral *) block).Invoke;");
indent--; print ("}");
if (shareNativeObjectActionInvoker) {
indent--;
print ("}} /* class {0} */", ti.NativeInvokerName);
continue;
}
print ("");
var string_pars = new StringBuilder ();
MakeSignatureFromParameterInfo (false, string_pars, mi, declaringType: null, parameters: parameters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,48 @@ class TrampolineEmitter (
public string SymbolNamespace => "ObjCRuntime";
public string SymbolName => "Trampolines";

bool emittedNativeObjectActionInvoker;

static bool CanShareNativeObjectActionInvoker (in TypeInfo typeInfo)
{
if (typeInfo.Delegate is not { ReturnType.IsVoid: true, Parameters.Length: 1 } info)
return false;

// Non-nullable parameters need checks that retain the original parameter name.
var parameter = info.Parameters [0];
return !parameter.IsByRef && parameter.BindAs is null &&
parameter.Type is { IsReferenceType: true, IsINativeObject: true, IsNullable: true };
}

void EmitNativeObjectActionInvoker (TabbedWriter<StringWriter> classBuilder)
{
// bgen and rgen can emit trampolines into the same assembly.
classBuilder.WriteRaw (
@"[UnmanagedFunctionPointerAttribute (CallingConvention.Cdecl)]
unsafe delegate void RgenNativeObjectAction (IntPtr block, NativeHandle obj);

internal class RgenNativeObjectActionInvoker : TrampolineBlockBase
{
readonly RgenNativeObjectAction invoker;

[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)]
public unsafe RgenNativeObjectActionInvoker (BlockLiteral *block) : base (block)
{
invoker = block->GetDelegateForBlock<RgenNativeObjectAction> ();
}

[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)]
protected void Invoke (INativeObject? obj)
{
var handle = obj.GetHandle ();
invoker (BlockPointer, handle);
GC.KeepAlive (obj);
}
}
"
);
}

/// <summary>
/// Generate the static helper class for the trampoline uses to bridge the native block invocation.
/// </summary>
Expand Down Expand Up @@ -110,15 +152,24 @@ public bool TryEmitNativeInvocationClass (in TypeInfo typeInfo, string trampolin
var delegateName = Nomenclator.GetTrampolineClassName (trampolineName, Nomenclator.TrampolineClassType.DelegateType);
var delegateIdentifier = Nomenclator.GetTrampolineClassName (trampolineName, Nomenclator.TrampolineClassType.DelegateType);

using (var classBlock = classBuilder.CreateBlock ($"internal sealed class {className} : TrampolineBlockBase", true)) {
classBlock.WriteLine ($"{delegateName} {Nomenclator.GetNativeInvokerVariableName ()};");
var shareNativeObjectActionInvoker = CanShareNativeObjectActionInvoker (typeInfo);
if (shareNativeObjectActionInvoker && !emittedNativeObjectActionInvoker) {
EmitNativeObjectActionInvoker (classBuilder);
emittedNativeObjectActionInvoker = true;
}
if (shareNativeObjectActionInvoker)
delegateIdentifier = typeInfo.GetIdentifierSyntax ().ToString ();

using (var classBlock = classBuilder.CreateBlock ($"internal sealed class {className} : {(shareNativeObjectActionInvoker ? "RgenNativeObjectActionInvoker" : "TrampolineBlockBase")}", true)) {
if (!shareNativeObjectActionInvoker)
classBlock.WriteLine ($"{delegateName} {Nomenclator.GetNativeInvokerVariableName ()};");
classBlock.WriteLine (); // empty line for readability
// constructor
classBlock.WriteRaw (
$@"[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)]
public unsafe {className} ({BlockLiteral} *block) : base (block)
{{
invoker = block->GetDelegateForBlock<{delegateName}> ();
{(shareNativeObjectActionInvoker ? "" : $"invoker = block->GetDelegateForBlock<{delegateName}> ();")}
}}
"
);
Expand All @@ -131,11 +182,13 @@ public bool TryEmitNativeInvocationClass (in TypeInfo typeInfo, string trampolin
{{
if (block == IntPtr.Zero)
return null;
var del = ({delegateIdentifier}) GetExistingManagedDelegate (block);
var del = ({delegateIdentifier}{(shareNativeObjectActionInvoker ? "?" : "")}) GetExistingManagedDelegate (block);
return del ?? new {className} (({BlockLiteral} *) block).Invoke;
}}
"
);
if (shareNativeObjectActionInvoker)
return true;
classBlock.WriteLine (); // empty line for readability
// invoke method
using (var invokeBlock = classBlock.CreateBlock (GetTrampolineNativeInvokeSignature (typeInfo).ToString (), true)) {
Expand Down
38 changes: 38 additions & 0 deletions tests/bgen/BGenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,43 @@ public void NativeEnum (Profile profile)
bgen.AssertExecute ("build");
}

[Test]
[TestCase (Profile.iOS)]
[TestCase (Profile.macOSMobile)]
public void SharedNativeObjectTrampolines (Profile profile)
{
var bgen = BuildFile (profile, "shared-native-object-trampolines.cs");
var types = bgen.ApiAssembly.MainModule.GetType ("ObjCRuntime", "Trampolines").NestedTypes;
var shared = types.Single (v => v.Name == "NativeObjectActionInvoker");
var invoke = shared.Methods.Single (v => v.Name == "Invoke");
Assert.That (invoke.Parameters.Single ().ParameterType.FullName, Is.EqualTo ("ObjCRuntime.INativeObject"), "Shared parameter type");
var calls = invoke.Body.Instructions.Where (v => v.OpCode.FlowControl == FlowControl.Call)
.Select (v => (MethodReference) v.Operand).ToArray ();
Assert.That (calls.Select (v => v.Name), Is.EqualTo (new [] { "GetHandle", "get_BlockPointer", "Invoke", "KeepAlive" }), "Null conversion and lifetime");

foreach (var name in new [] { "ObjectCallback", "StringObjectCallback", "ImageCallback", "ProtocolCallback", "NestedObjectCallback" }) {
var invoker = types.Single (v => v.Name == "NID" + name);
Assert.That (invoker.BaseType.FullName, Is.EqualTo (shared.FullName), name + " base");
Assert.That (invoker.Methods.Any (v => v.Name == "Invoke"), Is.False, name + " has no duplicate Invoke");
Assert.That (invoker.Fields, Is.Empty, name + " has no duplicate invoker field");
var create = invoker.Methods.Single (v => v.Name == "Create");
Assert.That (create.ReturnType.FullName, Is.EqualTo ("SharedNativeObjectTrampolines." + name), name + " factory return type");
Assert.That (create.Body.Instructions.Any (v => v.OpCode == OpCodes.Ldftn &&
((MethodReference) v.Operand).DeclaringType.FullName == shared.FullName), Is.True, name + " binds shared Invoke");
Assert.That (create.Body.Instructions.Any (v => v.Operand is MethodReference method &&
method.Name == "GetExistingManagedDelegate"), Is.True, name + " preserves managed delegates");
var bridge = types.Single (v => v.Name == "SD" + name).Methods.Single (v => v.Name == "Invoke");
var attribute = bridge.CustomAttributes.Single (v => v.AttributeType.Name == "UserDelegateTypeAttribute");
Assert.That (((TypeReference) attribute.ConstructorArguments [0].Value).Name, Is.EqualTo (name), name + " bridge metadata");
}

foreach (var name in new [] { "ReturningCallback", "RefCallback", "ArrayCallback", "StringCallback", "MultipleCallback", "ValueCallback", "NestedCallback" }) {
var invoker = types.Single (v => v.Name == "NID" + name);
Assert.That (invoker.BaseType.FullName, Is.EqualTo ("ObjCRuntime.TrampolineBlockBase"), name + " remains specialized");
Assert.That (invoker.Methods.Any (v => v.Name == "Invoke"), Is.True, name + " has specialized Invoke");
}
}

[Test]
public void DelegateWithINativeObjectReturnType ()
{
Expand All @@ -1126,6 +1163,7 @@ public void DelegateWithINativeObjectReturnType ()
// Assert that the return type from the delegate is IntPtr
var type = bgen.ApiAssembly.MainModule.GetType ("ObjCRuntime", "Trampolines").NestedTypes.First (v => v.Name == "DMyHandler");
Assert.That (type, Is.Not.Null, "DMyHandler");
Assert.That (type.DeclaringType.NestedTypes.Any (v => v.Name == "NativeObjectActionInvoker"), Is.False, "No shared invoker without compatible callbacks");
var method = type.Methods.First (v => v.Name == "Invoke");
Assert.That (method.ReturnType.FullName, Is.EqualTo ("ObjCRuntime.NativeHandle"), "Return type");
}
Expand Down
57 changes: 57 additions & 0 deletions tests/bgen/tests/shared-native-object-trampolines.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using CoreGraphics;
using Foundation;
using ObjCRuntime;

namespace SharedNativeObjectTrampolines {
delegate void ObjectCallback (NSObject value);
delegate void StringObjectCallback ([NullAllowed] NSString value);
delegate void ImageCallback (CGImage value);
delegate void ProtocolCallback (INSCopying value);
delegate bool ReturningCallback (NSObject value);
delegate void RefCallback (ref NSObject value);
delegate void ArrayCallback (NSObject [] value);
delegate void StringCallback (string value);
delegate void MultipleCallback (NSObject first, NSObject second);
delegate void ValueCallback (int value);
delegate void NestedObjectCallback (NSUrl value);
delegate void NestedCallback ([BlockCallback] NestedObjectCallback callback);

[BaseType (typeof (NSObject))]
interface Callbacks {
[Export ("objectCallback")]
ObjectCallback ObjectCallback { get; set; }

[Export ("stringObjectCallback")]
StringObjectCallback StringObjectCallback { get; set; }

[Export ("imageCallback")]
ImageCallback ImageCallback { get; set; }

[Export ("protocolCallback")]
ProtocolCallback ProtocolCallback { get; set; }

[Export ("returningCallback")]
ReturningCallback ReturningCallback { get; set; }

[Export ("refCallback")]
RefCallback RefCallback { get; set; }

[Export ("arrayCallback")]
ArrayCallback ArrayCallback { get; set; }

[Export ("stringCallback")]
StringCallback StringCallback { get; set; }

[Export ("multipleCallback")]
MultipleCallback MultipleCallback { get; set; }

[Export ("valueCallback")]
ValueCallback ValueCallback { get; set; }

[Export ("nestedCallback")]
NestedCallback NestedCallback { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Macios.Generator.Context;
using Microsoft.Macios.Generator.Emitters;
using Microsoft.Macios.Generator.IO;
using Xamarin.Utils;
using Xunit;
using TypeInfo = Microsoft.Macios.Generator.DataModel.TypeInfo;

namespace Microsoft.Macios.Generator.Tests.Emitters;

public class TrampolineEmitterTests : BaseGeneratorTestClass {
[Theory]
[InlineData (ApplePlatform.iOS)]
[InlineData (ApplePlatform.MacOSX)]
public void SharedNativeObjectInvokers (ApplePlatform platform)
{
const string source = """
#nullable enable
using Foundation;
using CoreGraphics;
namespace Test {
public delegate void ObjectCallback (NSObject? value);
public delegate void ImageCallback (CGImage? image);
public delegate void ProtocolCallback (INSCopying? value);
public delegate void NonNullableCallback (NSObject value);
public delegate bool ReturningCallback (NSObject? value);
public delegate void RefCallback (ref NSObject? value);
public delegate void ArrayCallback (NSObject []? value);
public delegate void StringCallback (string? value);
public delegate void MultipleCallback (NSObject? first, NSObject? second);
public delegate void ValueCallback (int value);
}
""";
var (compilation, trees) = CreateCompilation (platform, sources: source);
var model = compilation.GetSemanticModel (trees [0]);
using var builder = new TabbedStringBuilder (new ());
var emitter = new TrampolineEmitter (new RootContext (model), builder);
var types = trees [0].GetRoot ().DescendantNodes ().OfType<DelegateDeclarationSyntax> ()
.Select (declaration => new TypeInfo (model.GetDeclaredSymbol (declaration)!)).ToHashSet ();
Assert.True (emitter.TryEmit (types, out var diagnostics));
Assert.Null (diagnostics);
var root = CSharpSyntaxTree.ParseText (builder.ToCode ()).GetRoot ();
var shared = Assert.Single (root.DescendantNodes ().OfType<ClassDeclarationSyntax> (),
c => c.Identifier.Text == "RgenNativeObjectActionInvoker");
var invoke = Assert.Single (shared.Members.OfType<MethodDeclarationSyntax> ());
Assert.Equal ("INativeObject?", invoke.ParameterList.Parameters.Single ().Type!.ToString ());
Assert.Contains ("obj.GetHandle ()", invoke.ToString ());
Assert.Contains ("GC.KeepAlive (obj)", invoke.ToString ());
var sharedClasses = new [] { shared }.ToList ();
foreach (var name in new [] { "ObjectCallback", "ImageCallback", "ProtocolCallback" }) {
var invoker = Assert.Single (root.DescendantNodes ().OfType<ClassDeclarationSyntax> (),
c => c.Identifier.Text == "NID" + name);
Assert.Equal ("RgenNativeObjectActionInvoker", invoker.BaseList!.Types.Single ().ToString ());
Assert.DoesNotContain (invoker.Members.OfType<MethodDeclarationSyntax> (), m => m.Identifier.Text == "Invoke");
var create = Assert.Single (invoker.Members.OfType<MethodDeclarationSyntax> ());
Assert.Equal ($"global::Test.{name}?", create.ReturnType.ToString ());
Assert.Contains ("GetExistingManagedDelegate (block)", create.ToString ());
sharedClasses.Add (invoker);
}
foreach (var name in new [] { "NonNullableCallback", "ReturningCallback", "RefCallback", "ArrayCallback", "StringCallback", "MultipleCallback", "ValueCallback" }) {
var invoker = Assert.Single (root.DescendantNodes ().OfType<ClassDeclarationSyntax> (),
c => c.Identifier.Text == "NID" + name);
Assert.Equal ("TrampolineBlockBase", invoker.BaseList!.Types.Single ().ToString ());
Assert.Contains (invoker.Members.OfType<MethodDeclarationSyntax> (), m => m.Identifier.Text == "Invoke");
}

var nativeDelegate = Assert.Single (root.DescendantNodes ().OfType<DelegateDeclarationSyntax> (),
d => d.Identifier.Text == "RgenNativeObjectAction");
var sharedSource = "#nullable enable\nusing System;\nusing System.Runtime.InteropServices;\nusing Foundation;\n" +
"namespace ObjCRuntime { static partial class Trampolines {\n" +
nativeDelegate.ToFullString () + string.Join ("\n", sharedClasses.Select (c => c.ToFullString ())) + "\n} }";
using var assembly = new MemoryStream ();
var result = compilation.AddSyntaxTrees (CSharpSyntaxTree.ParseText (sharedSource)).Emit (assembly);
Assert.True (result.Success, string.Join ("\n", result.Diagnostics.Where (d => d.Severity == DiagnosticSeverity.Error)));
}
}
Loading