Skip to content

Commit

Permalink
[ObjCRuntime] Add support for function pointers to BlockLiteral.
Browse files Browse the repository at this point in the history
* Make BlockLiteral disposable and possible to use in a 'using' statement.
  This also meant making the CleanupBlock method support being called multiple
  times.
  • Loading branch information
rolfbjarne committed Oct 18, 2022
1 parent cc7e1ca commit 3139dfb
Showing 1 changed file with 128 additions and 28 deletions.
156 changes: 128 additions & 28 deletions src/ObjCRuntime/Blocks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ struct XamarinBlockDescriptor {
}

[StructLayout (LayoutKind.Sequential)]
public unsafe struct BlockLiteral {
#if XAMCORE_5_0
// The presence of this Dispose method is enough to be able to do a 'using var block = new BlockLiteral ()' in C# due to pattern-based using for 'ref structs':
// Ref: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/using#pattern-based-using
public unsafe ref struct BlockLiteral
#else
public unsafe struct BlockLiteral : IDisposable {
#endif
#pragma warning disable 169
IntPtr isa;
BlockFlags flags;
Expand All @@ -82,8 +88,61 @@ static IntPtr NSConcreteStackBlock {
[DllImport ("__Internal")]
static extern IntPtr xamarin_get_block_descriptor ();

static MethodInfo FindTrampoline (Type trampolineType, string trampolineMethod)
{
var rv = trampolineType.GetMethod (trampolineMethod, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);

if (rv is null)
throw new InvalidOperationException ($"Unable to find the method {trampolineMethod} in the type {trampolineType}");

return rv;

}

public BlockLiteral (void *trampoline, Delegate target, Type trampolineType, string trampolineMethod)
: this (trampoline, target, FindTrampoline (trampolineType, trampolineMethod))
{
}

public BlockLiteral (void *trampoline, Delegate target, MethodInfo trampolineMethod)
{
isa = IntPtr.Zero;
flags = (BlockFlags) 0;
reserved = 0;
invoke = IntPtr.Zero;
block_descriptor = IntPtr.Zero;
local_handle = IntPtr.Zero;
global_handle = IntPtr.Zero;
SetupBlock (trampoline, target, trampolineMethod);
}

// trampoline: a function pointer to a method with the UnmanagedCallersOnly attribute.
// target: can be looked up using the first argument to the trampoline (which is a BlockLiteral*/IntPtr).
public void SetupBlock (void* trampoline, Delegate target, MethodInfo trampolineMethod)
{
var functionPointer = trampolineMethod.MethodHandle.GetFunctionPointer ();
if (functionPointer != (IntPtr) trampoline)
throw new ArgumentException ($"BlockLiteral.SetupBlock (): The trampoline and the trampolineMethod refer to different methods.");

// TODO: assert that trampoline has an UnmanagedCallersOnly attribute (by looking at trampolineMethod) + that the first argument is either BlockLiteral* or IntPtr.
var signature = Runtime.ComputeSignature (trampolineMethod, true);
SetupBlockImpl ((IntPtr) trampoline, target, System.Text.Encoding.UTF8.GetBytes (signature));
}

// trampoline: a function pointer to a method with the UnmanagedCallersOnly attribute.
public void SetupBlock (void* trampoline, Delegate target, string signature)
{
SetupBlockImpl ((IntPtr) trampoline, target, System.Text.Encoding.UTF8.GetBytes (signature));
}

// trampoline: a function pointer to a method with the UnmanagedCallersOnly attribute.
public void SetupBlock (void* trampoline, Delegate target, byte[] utf8Signature)
{
SetupBlockImpl ((IntPtr) trampoline, target, utf8Signature);
}

[BindingImpl (BindingImplOptions.Optimizable)]
void SetupBlock (Delegate trampoline, Delegate userDelegate, bool safe)
void SetupBlock (Delegate trampoline, Delegate target, bool safe)
{
if (!Runtime.DynamicRegistrationSupported)
throw ErrorHelper.CreateError (8026, "BlockLiteral.SetupBlock is not supported when the dynamic registrar has been linked away.");
Expand All @@ -110,23 +169,37 @@ void SetupBlock (Delegate trampoline, Delegate userDelegate, bool safe)
}

var signature = Runtime.ComputeSignature (userMethod, blockSignature);
SetupBlockImpl (trampoline, userDelegate, safe, signature);
SetupBlockImpl (trampoline, target, safe, System.Text.Encoding.UTF8.GetBytes (signature));
}

unsafe void SetupBlockImpl (Delegate trampoline, Delegate target, bool safe, string signature)
{
SetupBlockImpl (trampoline, target, safe, System.Text.Encoding.UTF8.GetBytes (signature));
}

unsafe void SetupBlockImpl (Delegate trampoline, Delegate target, bool safe, byte[] utf8Signature)
{
var invoke = Marshal.GetFunctionPointerForDelegate (trampoline);
SetupBlockImpl (invoke, GetContext (trampoline, target, safe), utf8Signature);
}

static object GetContext (Delegate trampoline, Delegate target, bool safe)
{
if (safe) {
return new Tuple<Delegate, Delegate> (trampoline, target);
} else {
return target;
}
}

// This method is not to be called manually by user code.
// This is enforced by making it private. If the SetupBlock optimization is enabled,
// the linker will make it public so that it's callable from optimized user code.
unsafe void SetupBlockImpl (Delegate trampoline, Delegate userDelegate, bool safe, string signature)
unsafe void SetupBlockImpl (IntPtr invokeMethod, object context, byte[] utf8Signature)
{
isa = NSConcreteStackBlock;
invoke = Marshal.GetFunctionPointerForDelegate (trampoline);
object delegates;
if (safe) {
delegates = new Tuple<Delegate, Delegate> (trampoline, userDelegate);
} else {
delegates = userDelegate;
}
local_handle = (IntPtr) GCHandle.Alloc (delegates);
invoke = invokeMethod;
local_handle = (IntPtr) GCHandle.Alloc (context);
global_handle = IntPtr.Zero;
flags = BlockFlags.BLOCK_HAS_COPY_DISPOSE | BlockFlags.BLOCK_HAS_SIGNATURE;

Expand All @@ -137,8 +210,9 @@ unsafe void SetupBlockImpl (Delegate trampoline, Delegate userDelegate, bool saf
// for the signature if we can avoid it). One descriptor is allocated for every
// Block; this is potentially something the static registrar can fix, since it
// should know every possible trampoline signature.
var bytes = System.Text.Encoding.UTF8.GetBytes (signature);
var desclen = sizeof (XamarinBlockDescriptor) + bytes.Length + 1 /* null character */;
var bytes = utf8Signature;
var hasNull = utf8Signature [utf8Signature.Length - 1] == 0;
var desclen = sizeof (XamarinBlockDescriptor) + bytes.Length + (hasNull ? 0 : 1 /* null character */);
var descptr = Marshal.AllocHGlobal (desclen);

block_descriptor = descptr;
Expand All @@ -147,7 +221,8 @@ unsafe void SetupBlockImpl (Delegate trampoline, Delegate userDelegate, bool saf
xblock_descriptor->descriptor.signature = descptr + sizeof (BlockDescriptor) + 4 /* signature_length */;
xblock_descriptor->ref_count = 1;
Marshal.Copy (bytes, 0, xblock_descriptor->descriptor.signature, bytes.Length);
Marshal.WriteByte (xblock_descriptor->descriptor.signature + bytes.Length, 0); // null terminate string
if (!hasNull)
Marshal.WriteByte (xblock_descriptor->descriptor.signature + bytes.Length, 0); // null terminate string
}

// trampoline must be static, and someone else needs to keep a ref to it
Expand Down Expand Up @@ -202,23 +277,36 @@ public void SetupBlock (Delegate trampoline, Delegate userDelegate)

public void CleanupBlock ()
{
GCHandle.FromIntPtr (local_handle).Free ();
var xblock_descriptor = (XamarinBlockDescriptor *) block_descriptor;
#pragma warning disable 420
// CS0420: A volatile field references will not be treated as volatile
// Documentation says: "A volatile field should not normally be passed using a ref or out parameter, since it will not be treated as volatile within the scope of the function. There are exceptions to this, such as when calling an interlocked API."
// So ignoring the warning, since it's a documented exception.
var rc = Interlocked.Decrement (ref xblock_descriptor->ref_count);
#pragma warning restore 420

if (rc == 0)
Marshal.FreeHGlobal (block_descriptor);
if (local_handle != IntPtr.Zero) {
GCHandle.FromIntPtr (local_handle).Free ();
local_handle = IntPtr.Zero;
}

if (block_descriptor != IntPtr.Zero) {
var xblock_descriptor = (XamarinBlockDescriptor *) block_descriptor;
#pragma warning disable 420
// CS0420: A volatile field references will not be treated as volatile
// Documentation says: "A volatile field should not normally be passed using a ref or out parameter, since it will not be treated as volatile within the scope of the function. There are exceptions to this, such as when calling an interlocked API."
// So ignoring the warning, since it's a documented exception.
var rc = Interlocked.Decrement (ref xblock_descriptor->ref_count);
#pragma warning restore 420

if (rc == 0)
Marshal.FreeHGlobal (block_descriptor);
block_descriptor = IntPtr.Zero;
}
}

public object Target {
public object Context {
get {
var handle = global_handle != IntPtr.Zero ? global_handle : local_handle;
var target = GCHandle.FromIntPtr (handle).Target;
return GCHandle.FromIntPtr (handle).Target;
}
}

public object Target {
get {
var target = Context;
var tuple = target as Tuple<Delegate, Delegate>;
if (tuple != null)
return tuple.Item2;
Expand Down Expand Up @@ -376,7 +464,19 @@ internal unsafe static void SimpleCall (Action callbackToInvoke, Action <IntPtr
}
}

#endif // !COREBUILD

#if XAMCORE_5_0
public void Dispose ()
#else
void IDisposable.Dispose ()
#endif
{
#if !COREBUILD
CleanupBlock ();
#endif
}

}

#if !COREBUILD
Expand Down

0 comments on commit 3139dfb

Please sign in to comment.