Skip to content
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

Add a managed static registrar. Fixes #17324. #18268

Merged
merged 68 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
527cba2
[docs] Document the managed static registrar.
rolfbjarne May 5, 2023
7c7637e
[dotnet-linker] Reduce a bit of code duplication.
rolfbjarne May 8, 2023
9e11297
[dotnet-linker] Don't fail trimming if all the exceptions we collect …
rolfbjarne May 8, 2023
eb01507
[dotnet-linker] Unify exception handling to go through the LinkerConf…
rolfbjarne May 8, 2023
4351674
[dotnet-linker] Add a way for ConfigurationAwareStep subclasses to re…
rolfbjarne May 8, 2023
3ac9b8f
[xharness] Add new variations using the managed static registrar for …
rolfbjarne Jan 25, 2023
872af5d
[static registrar] Refactor code to make it easier to reuse code late…
rolfbjarne Jan 25, 2023
e66f82d
[static registrar] Refactor code to make it easier to reuse code late…
rolfbjarne Jan 25, 2023
a1410ac
[static registrar] Refactor code to make it easier to reuse code late…
May 5, 2023
cfb248e
[tools] Add a ManagedStatic registrar mode.
rolfbjarne Jan 25, 2023
34264cd
[dotnet] Add an 'IsManagedStaticRegistrar' feature to the linker.
rolfbjarne Jan 25, 2023
bf639a9
[dotnet-linker] Add the scaffolding for a ManagedRegistrarStep and a …
rolfbjarne Jan 25, 2023
57d40d9
[dotnet-linker] Don't do anything in ManagedRegistrarStep unless the …
rolfbjarne May 10, 2023
e39c6fb
[dotnet-linker] Rearrange registration and generation in the static r…
rolfbjarne Jan 25, 2023
a1e0e30
[registrar] Make some API from the registrar public so that the manag…
rolfbjarne Jan 25, 2023
43b88af
[src] Fix comparison between signed and unsigned int.
rolfbjarne Jan 25, 2023
8f1fb22
[static registrar] Add support for generating block syntax in Objecti…
May 5, 2023
70a39a7
[static registrar] Move token reference creation a little bit later.
May 5, 2023
53d7bc5
[tools] Move code to compute block signatures to the static registrar.
May 5, 2023
dc56054
[registrar] Add an HasCustomAttribute overload that returns the found…
May 5, 2023
47eddc5
[dotnet-linker] Add extension methods for making IL emission easier w…
May 5, 2023
2a40824
[dotnet-linker] Add a helper class for keeping track of methods and t…
May 5, 2023
72c654e
[registrar] Refactor code to determine if a method is a property acce…
May 5, 2023
4ab321d
[dotnet-linker] Remove trimmed API from the registered types before g…
May 5, 2023
4bd045e
[static registrar] Refactor code to make it easier to reuse code late…
May 8, 2023
cbfc591
[src] Refactor Class.ResolveToken to take the assembly as a parameter.
Apr 28, 2023
fb6ed7b
[runtime] Add an API to look up the native symbol for an [UnmanagedCa…
rolfbjarne Jan 25, 2023
382ebae
[tools] Add a managed static registrar. Fixes #17324.
May 9, 2023
9bcc96e
[static registrar] Implement support for calling the generated Unmana…
May 5, 2023
b8ccbad
[src] Add helper methods for the managed static registrar
May 5, 2023
8f55662
[registrar] We might link the [Protocol] attribute away, so store it …
May 5, 2023
ed4eb42
[src] Track selector and method better to provide more helpful error …
rolfbjarne Jan 25, 2023
a2f5e16
[tests] Improve the current registrar detection in the tests
rolfbjarne Jan 25, 2023
1deba2c
[tests] Add a check for the managed static registrar
rolfbjarne Jan 25, 2023
eb62964
[tests] Update to work with the managed static registrar
rolfbjarne Jan 25, 2023
4f5fa5e
[monotouch-test] Use MidiThruConnectionEndpoint instead of MidiCIDevi…
rolfbjarne May 8, 2023
17b2d37
[tests] Adjust to cope with slightly different errors reported when u…
rolfbjarne Apr 26, 2023
00a8228
[linker] Don't optimize calls to BlockLiteral.SetupBlock in BlockLite…
rolfbjarne May 11, 2023
dd64974
Update docs/managed-static-registrar.md
rolfbjarne May 12, 2023
5693250
Update docs/managed-static-registrar.md
rolfbjarne May 12, 2023
b4ea025
[src] Remove extraneous tab.
rolfbjarne May 12, 2023
3da0809
[dotnet-linker] Don't prefix the exported entry point from an Unmanag…
rolfbjarne May 12, 2023
f32c9e1
[tools] Simplify code a little bit.
rolfbjarne May 12, 2023
dd2252f
Merge remote-tracking branch 'origin/main' into msr
rolfbjarne May 12, 2023
3a8b988
Apply suggestions from code review
rolfbjarne May 15, 2023
7a5fca4
Update runtime/xamarin/runtime.h
rolfbjarne May 15, 2023
a04a1d4
Mark type forwarders when saving assemblies
rolfbjarne May 15, 2023
6d33409
[dotnet-linker] Improve code in AppBundleRewriter according to review.
rolfbjarne May 15, 2023
065da5f
[tests] Use Enum.HasFlag according to reviews.
rolfbjarne May 15, 2023
587058f
[tools] Simplify code a little bit.
rolfbjarne May 15, 2023
4053da7
[tools] Add availability attributes in new code.
rolfbjarne May 15, 2023
e4830a8
[dotnet-linker] Improve method naming a bit according to reviews.
rolfbjarne May 15, 2023
fd67384
[dotnet-linker] Add missing newline.
rolfbjarne May 15, 2023
2828d7f
[dotnet-tool] Expand a bit in a comment.
rolfbjarne May 15, 2023
25ad7b2
[dotnet-linker] Always use 'IntPtr' as the type of for the first argu…
rolfbjarne May 15, 2023
429b8e3
[dotnet-linker] Improve a comment.
rolfbjarne May 15, 2023
50ba357
[runtime] Change NSObject.AllocateNSObject<T> to not take the flags p…
rolfbjarne May 15, 2023
a248260
Auto-format source code
May 15, 2023
643015e
[dotnet-linker] Make sure local variables are initialized before use …
rolfbjarne May 17, 2023
799095e
[dotnet-linker] Ensure we call Runtime.CopyAndAutorelease in all appl…
rolfbjarne May 18, 2023
a7371cf
[dotnet-linker] Remove dead code.
rolfbjarne May 18, 2023
469fdb5
[dotnet-linker] Fix detecting when we must call the object-based Runt…
May 18, 2023
ed7b4b6
[dotnet-linker] Keep better track of when we're calling MethodBase.In…
May 18, 2023
aa75c70
[dotnet-linker] Keep track of when we called MethodBase.Invoke and an…
May 18, 2023
d540050
[dotnet-linker] Add a missing cast to make generated code more verifi…
May 18, 2023
666ec96
[dotnet-linker] Simplify generated code for constructors of generic t…
rolfbjarne May 18, 2023
03488a2
Auto-format source code
May 18, 2023
65590ba
Merge remote-tracking branch 'origin/main' into msr
rolfbjarne May 18, 2023
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
227 changes: 227 additions & 0 deletions docs/managed-static-registrar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# Managed static registrar

The managed static registrar is a variation of the static registrar where we
don't use features the NativeAOT compiler doesn't support (most notably
metadata tokens).

It also takes advantage of new features in C# and managed code since the
original static registrar code was written - in particular it tries to do as
much as possible in managed code instead of native code, as well as various
other performance improvements. The actual performance characteristics
compared to the original static registrar will vary between the specific
exported method signatures, but in general it's expected that method calls
from native code to managed code will be faster.

In order to make the managed static registrar easily testable and debuggable,
it's also implemented for the other runtimes as well (Mono and CoreCLR as
well), as well as when not using AOT in any form.
rolfbjarne marked this conversation as resolved.
Show resolved Hide resolved

## Design

### Exported methods

For each method exported to Objective-C, the managed static registrar will
generate a managed method we'll call directly from native code, and which does
all the marshalling.

This method will have the [UnmanagedCallersOnly] attribute, so that it doesn't
need any additional marshalling from the managed runtime - which makes it
possible to obtain a native function pointer for it. It will also have a
native entry point, which means that for AOT we can just directly call it from
the generated Objective-C code.

Given the following method:

```csharp
class AppDelegate : NSObject, IUIApplicationDelegate {
// this method is written by the app developer
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
// ...
}
}
```

The managed static registrar will add the following method to the `AppDelegate` class:

```csharp
class AppDelegate {
[UnmanagedCallersOnly (EntryPoint = "__registrar__uiapplicationdelegate_didFinishLaunching")]
static byte __registrar__DidFinishLaunchingWithOptions (IntPtr handle, IntPtr selector, IntPtr p0, IntPtr p1)
{
var obj = Runtime.GetNSObject (handle);
var p0Obj = (UIApplication) Runtime.GetNSObject (p0);
var p1Obj = (NSDictionary) Runtime.GetNSObject (p1);
var rv = obj.DidFinishLaunchingWithOptions (p0Obj, p1Obj);
return rv ? (byte) 1 : (byte) 0;
}
}
```

and the generated Objective-C code will look something like this:

```objective-c
extern BOOL __registrar__uiapplicationdelegate_init (AppDelegate self, SEL _cmd, UIApplication* p0, NSDictionary* p1);

@interface AppDelegate : NSObject<UIApplicationDelegate, UIApplicationDelegate> {
}
-(BOOL) application:(UIApplication *)p0 didFinishLaunchingWithOptions:(NSDictionary *)p1;
@end
@implementation AppDelegate {
}
-(BOOL) application:(UIApplication *)p0 didFinishLaunchingWithOptions:(NSDictionary *)p1
{
return __registrar__uiapplicationdelegate_didFinishLaunching (self, _cmd, p0, p1);
}
@end
```

Note: the actual code is somewhat more complex in order to properly support
managed exceptions and a few other corner cases.

### Type mapping

The runtime needs to quickly and efficiently do lookups between an Objective-C
type and the corresponding managed type. In order to support this, the managed
static registrar will add lookup tables in each assembly. The managed static
registrar will create a numeric ID for each managed type, which is then
emitted into the generated Objective-C code, and which we can use to look up
the corresponding managed type. There is also a table in Objective-C that maps
between the numeric ID and the corresponding Objective-C type.

We also need to be able to find the wrapper type for interfaces representing
Objective-C protocols - this is accomplished by generating a table in
unmanaged code that maps the ID for the interface to the ID for the wrapper
type.

This is all supported by the `ObjCRuntime.IManagedRegistrar.LookTypeId` and
rolfbjarne marked this conversation as resolved.
Show resolved Hide resolved
`ObjCRuntime.IManagedRegistrar.Lookup` methods.
rolfbjarne marked this conversation as resolved.
Show resolved Hide resolved

Note that in many ways the type ID is similar to the metadata token for a type
(and is sometimes referred to as such in the code, especially code that
already existed before the managed static registrar was implemented).

### Method mapping

When AOT-compiling code, the generated Objective-C code can call the entry
point for the UnmanagedCallersOnly trampoline directly (the AOT compiler will
emit a native symbol with the name of the entry point).

However, when no AOT-compiling code, the generated Objective-C code needs to
rolfbjarne marked this conversation as resolved.
Show resolved Hide resolved
find the function pointer for the UnmanagedCallersOnly methods. This is
implemented using another lookup table in managed code.

For technical reasons, this implemented using multiple levels of functions if
there are a significant number of UnmanagedCallersOnly methods, because it
seems the JIT will compile the target for every function pointer in a method,
rolfbjarne marked this conversation as resolved.
Show resolved Hide resolved
even if tha function pointer isn't loaded at runtime. This means that if
rolfbjarne marked this conversation as resolved.
Show resolved Hide resolved
there's 1.000 methods in the lookup table, the JIT will have to compile all
the 1.000 methods the first time the lookup method is called if the lookup was
implemented in a single function, even if the lookup method will eventually
just find a single callback.

This might be easier to describe with some code.

Instead of this:

```csharp
class __Registrar_Callbacks__ {
IntPtr LookupUnmanagedFunction (int id)
{
switch (id) {
case 0: return (IntPtr) (delegate* unmanaged<void>) &Callback0;
case 1: return (IntPtr) (delegate* unmanaged<void>) &Callback1;
...
case 999: return (IntPtr) (delegate* unmanaged<void>) &Callback999;
}
return (IntPtr) -1);
}
}
```

we do this instead:

```csharp
class __Registrar_Callbacks__ {
IntPtr LookupUnmanagedFunction (int id)
{
if (id < 100)
return LookupUnmanagedFunction_0 (id);
if (id < 200)
return LookupUnmanagedFunction_1 (id);
...
if (id < 1000)
LookupUnmanagedFunction_9 (id);
return (IntPtr) -1;
}

IntPtr LookupUnmanagedFunction_0 (int id)
{
switch (id) {
case 0: return (IntPtr) (delegate* unmanaged<void>) &Callback0;
case 1: return (IntPtr) (delegate* unmanaged<void>) &Callback1;
/// ...
case 9: return (IntPtr) (delegate* unmanaged<void>) &Callback9;
}
return (IntPtr) -1;
}


IntPtr LookupUnmanagedFunction_1 (int id)
{
switch (id) {
case 10: return (IntPtr) (delegate* unmanaged<void>) &Callback10;
case 11: return (IntPtr) (delegate* unmanaged<void>) &Callback11;
/// ...
case 19: return (IntPtr) (delegate* unmanaged<void>) &Callback19;
}
return (IntPtr) -1;
}
}
```


### Generation

All the generated IL is done in two separate custom linker steps. The first
one, ManagedRegistrarStep, will generate the UnmanagedCallersOnly trampolines
for every method exported to Objective-C. This happens before the trimmed has
rolfbjarne marked this conversation as resolved.
Show resolved Hide resolved
done any work (i.e. before marking), because the generated code will cause
more code to be marked (and this way we don't have to replicate what the
trimmer does when it traverses IL and metadata to figure out what else to
mark).

The trimmer will then trim away any UnmanagedCallersOnly trampoline that's no
longer needed because the target method has been trimmed away.

On the other hand, the lookup tables for the type mapping is done after
rolfbjarne marked this conversation as resolved.
Show resolved Hide resolved
trimming, because we only want to add types that aren't trimmed away to the
lookup tables (otherwise we'd end up causing all those types to be kept).

## Interpreter / JIT

When not using the AOT compiler, we need to look up the native entry points
for UnmanagedCallersOnly methods at runtime. In order to support this, the
managed static registrar will add lookup tables in each assembly. The managed
static registrar will create a numeric ID for each UnmanagedCallersOnly
method, which is then emitted into the generated Objective-C code, and which
we can use to look up the managed UnmanagedCallersOnly method at runtime (in
the lookup table).

This is the `ObjCRuntime.IManagedRegistrar.LookupUnmanagedFunction` method.

## Performance

Preliminary testing shows the following:

### macOS

Calling an exported managed method from Objective-C is 3-6x faster for simple method signatures.

### Mac Catalyst

Calling an exported managed method from Objective-C is 30-50% faster for simple method signatures.

## References

* https://github.com/dotnet/runtime/issues/80912
10 changes: 10 additions & 0 deletions dotnet/targets/Xamarin.Shared.Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,10 @@
<_ExtraTrimmerArgs Condition="('$(_PlatformName)' == 'iOS' Or '$(_PlatformName)' == 'tvOS') And '$(_SdkIsSimulator)' == 'true'">$(_ExtraTrimmerArgs) --feature ObjCRuntime.Runtime.Arch.IsSimulator true</_ExtraTrimmerArgs>
<_ExtraTrimmerArgs Condition="('$(_PlatformName)' == 'iOS' Or '$(_PlatformName)' == 'tvOS') And '$(_SdkIsSimulator)' != 'true'">$(_ExtraTrimmerArgs) --feature ObjCRuntime.Runtime.Arch.IsSimulator false</_ExtraTrimmerArgs>

<!-- Set managed static registrar value -->
<_ExtraTrimmerArgs Condition="'$(Registrar)' == 'managed-static'">$(_ExtraTrimmerArgs) --feature ObjCRuntime.Runtime.IsManagedStaticRegistrar true</_ExtraTrimmerArgs>
<_ExtraTrimmerArgs Condition="'$(Registrar)' != 'managed-static'">$(_ExtraTrimmerArgs) --feature ObjCRuntime.Runtime.IsManagedStaticRegistrar false</_ExtraTrimmerArgs>

<!-- Enable serialization discovery. Ref: https://github.com/xamarin/xamarin-macios/issues/15676 -->
<_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --enable-serialization-discovery</_ExtraTrimmerArgs>

Expand Down Expand Up @@ -589,6 +593,7 @@
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.RegistrarRemovalTrackingStep" />
<!-- TODO: these steps should probably run after mark. -->
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.PreMarkDispatcher" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.ManagedRegistrarStep" Condition="'$(Registrar)' == 'managed-static'" />

<!--
IMarkHandlers which run during Mark
Expand All @@ -601,6 +606,11 @@
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.MarkDispatcher" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsHandler" />

<!--
pre-sweep custom steps
-->
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="SweepStep" Type="Xamarin.Linker.ManagedRegistrarLookupTablesStep" Condition="'$(Registrar)' == 'managed-static'" />

<!--
post-sweep custom steps
-->
Expand Down
8 changes: 8 additions & 0 deletions runtime/delegates.t4
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,14 @@
) {
WrappedManagedFunction = "InvokeConformsToProtocol",
},

new XDelegate ("void *", "IntPtr", "xamarin_lookup_unmanaged_function",
"const char *", "IntPtr", "assembly",
"const char *", "IntPtr", "symbol",
"int32_t", "int", "id"
) {
WrappedManagedFunction = "LookupUnmanagedFunction",
},
};
delegates.CalculateLengths ();
#><#+
Expand Down
36 changes: 35 additions & 1 deletion runtime/runtime.m
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@

enum InitializationFlags : int {
InitializationFlagsIsPartialStaticRegistrar = 0x01,
/* unused = 0x02,*/
InitializationFlagsIsManagedStaticRegistrar = 0x02,
/* unused = 0x04,*/
/* unused = 0x08,*/
InitializationFlagsIsSimulator = 0x10,
Expand Down Expand Up @@ -2736,6 +2736,30 @@ -(void) xamarinSetFlags: (enum XamarinGCHandleFlags) flags;
[message release];
}

void
xamarin_registrar_dlsym (void **function_pointer, const char *assembly, const char *symbol, int32_t id)
{
if (*function_pointer != NULL)
return;

*function_pointer = dlsym (RTLD_MAIN_ONLY, symbol);
if (*function_pointer != NULL)
return;

GCHandle exception_gchandle = INVALID_GCHANDLE;
*function_pointer = xamarin_lookup_unmanaged_function (assembly, symbol, id, &exception_gchandle);
if (*function_pointer != NULL)
return;

if (exception_gchandle != INVALID_GCHANDLE)
xamarin_process_managed_exception_gchandle (exception_gchandle);

// This shouldn't really happen
NSString *msg = [NSString stringWithFormat: @"Unable to load the symbol '%s' to call managed code: %@", symbol, xamarin_print_all_exceptions (exception_gchandle)];
NSLog (@"%@", msg);
@throw [[NSException alloc] initWithName: @"SymbolNotFoundException" reason: msg userInfo: NULL];
}

/*
* File/resource lookup for assemblies
*
Expand Down Expand Up @@ -3195,6 +3219,16 @@ -(enum XamarinGCHandleFlags) xamarinGetFlags
return xamarin_debug_mode;
}

void
xamarin_set_is_managed_static_registrar (bool value)
{
if (value) {
options.flags = (InitializationFlags) (options.flags | InitializationFlagsIsManagedStaticRegistrar);
} else {
options.flags = (InitializationFlags) (options.flags & ~InitializationFlagsIsManagedStaticRegistrar);
}
}

bool
xamarin_is_managed_exception_marshaling_disabled ()
{
Expand Down
10 changes: 10 additions & 0 deletions runtime/xamarin/runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ void xamarin_check_objc_type (id obj, Class expected_class, SEL sel, id self,
#endif

void xamarin_set_gc_pump_enabled (bool value);
void xamarin_set_is_managed_static_registrar (bool value);

void xamarin_process_nsexception (NSException *exc);
void xamarin_process_nsexception_using_mode (NSException *ns_exception, bool throwManagedAsDefault, GCHandle *output_exception);
Expand Down Expand Up @@ -295,6 +296,15 @@ void xamarin_printf (const char *format, ...);
void xamarin_vprintf (const char *format, va_list args);
void xamarin_install_log_callbacks ();

/*
* Looks up a native function pointer for a managed [UnmanagedCallersOnly] method.
* function_pointer: the return value, lookup will only be performed if this points to NULL.
* assembly: the assembly to look in. Might be NULL if the app was not built with support for loading additional assemblies at runtime.
* symbol: the symbol to loop up. Can be NULL to save space (this value isn't used except in error messages).
rolfbjarne marked this conversation as resolved.
Show resolved Hide resolved
* id: a numerical id for faster lookup (than doing string comparisons on the symbol name).
*/
void xamarin_registrar_dlsym (void **function_pointer, const char *assembly, const char *symbol, int32_t id);

/*
* Wrapper GCHandle functions that takes pointer sized handles instead of ints,
* so that we can adapt our code incrementally to use pointers instead of ints
Expand Down
25 changes: 25 additions & 0 deletions src/Foundation/NSArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,19 @@ static public T [] ArrayFromHandle<T> (NativeHandle handle) where T : class, INa
return ret;
}

static Array ArrayFromHandle (NativeHandle handle, Type elementType)
{
if (handle == NativeHandle.Zero)
return null;

var c = (int) GetCount (handle);
var rv = Array.CreateInstance (elementType, c);
for (int i = 0; i < c; i++) {
rv.SetValue (UnsafeGetItem (handle, (nuint) i, elementType), i);
}
return rv;
}

static public T [] EnumsFromHandle<T> (NativeHandle handle) where T : struct, IConvertible
{
if (handle == NativeHandle.Zero)
Expand Down Expand Up @@ -395,6 +408,18 @@ static T UnsafeGetItem<T> (NativeHandle handle, nuint index) where T : class, IN
return Runtime.GetINativeObject<T> (val, false);
}

static object UnsafeGetItem (NativeHandle handle, nuint index, Type type)
{
var val = GetAtIndex (handle, index);
// A native code could return NSArray with NSNull.Null elements
// and they should be valid for things like T : NSDate so we handle
// them as just null values inside the array
if (val == NSNull.Null.Handle)
return null;

return Runtime.GetINativeObject (val, false, type);
rolfbjarne marked this conversation as resolved.
Show resolved Hide resolved
}

// can return an INativeObject or an NSObject
public T GetItem<T> (nuint index) where T : class, INativeObject
{
Expand Down
Loading