Skip to content

Commit

Permalink
Merge remote-tracking branch 'rolfbjarne/msr' into HEAD
Browse files Browse the repository at this point in the history
  • Loading branch information
filipnavara committed May 12, 2023
2 parents ea1538f + dd2252f commit 37f321f
Show file tree
Hide file tree
Showing 54 changed files with 5,348 additions and 633 deletions.
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.

## 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
`ObjCRuntime.IManagedRegistrar.Lookup` methods.
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
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,
even if the function pointer isn't loaded at runtime. This means that if
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 trimmer has
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
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 @@ -559,6 +559,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 @@ -591,6 +595,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 @@ -603,6 +608,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).
* 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);
}

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

0 comments on commit 37f321f

Please sign in to comment.