Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
81edc50
[Foundation] Bind two overloads to create NSUrl instances from file p…
rolfbjarne May 11, 2023
7d43ff6
[monotouch-test] Adjust the CGEventTest to handle successful tap crea…
rolfbjarne May 11, 2023
f654664
[monotouch-test] Adjust ImageCaptioningTest to don't care if we get a…
rolfbjarne May 11, 2023
4679f5e
[src] Use NSLog instead of Console.WriteLine in tracing statements.
rolfbjarne May 10, 2023
1d926c7
[dotnet-linker] Add code to dump all the linker steps. REMOVE FROM FI…
rolfbjarne Apr 13, 2023
441a670
DEBUG launch.json
rolfbjarne Jan 25, 2023
ee95bc3
DEBUG tests
rolfbjarne Jan 25, 2023
66bb560
DEBUG add perf test
rolfbjarne May 10, 2023
dbdc393
[tools] Add helper tool to create launch.json for dotnet-linker.
rolfbjarne May 10, 2023
f03d452
DEBUG skip test that times out.
rolfbjarne May 10, 2023
c18a4d9
DEBUG skip test that crashes on device.
rolfbjarne May 10, 2023
20300b5
[docs] Document the managed static registrar a bit.
rolfbjarne May 5, 2023
97266a0
[dotnet-linker] Reduce a bit of code duplication.
rolfbjarne May 8, 2023
46c7497
[dotnet-linker] Don't fail trimming if all the exceptions we collect …
rolfbjarne May 8, 2023
3152967
[dotnet-linker] Unify exception handling to go through the LinkerConf…
rolfbjarne May 8, 2023
4669ea8
[dotnet-linker] Add a way for ConfigurationAwareStep subclasses to re…
rolfbjarne May 8, 2023
92036d8
[xharness] Add new variations using the managed static registrar for …
rolfbjarne Jan 25, 2023
1183276
[static registrar] Refactor code to make it easier to reuse code late…
rolfbjarne Jan 25, 2023
d425fd5
[static registrar] Refactor code to make it easier to reuse code late…
rolfbjarne Jan 25, 2023
b8e6775
[static registrar] Refactor code to make it easier to reuse code late…
May 5, 2023
dd3aea0
[tools] Add a ManagedStatic registrar mode.
rolfbjarne Jan 25, 2023
8ad3142
[dotnet] Add an 'IsManagedStaticRegistrar' feature to the linker.
rolfbjarne Jan 25, 2023
b7117d3
[dotnet-linker] Add the scaffolding for a ManagedRegistrarStep and a …
rolfbjarne Jan 25, 2023
a1e8851
[dotnet-linker] Don't do anything in ManagedRegistrarStep unless the …
rolfbjarne May 10, 2023
feae3dd
[dotnet-linker] Rearrange registration and generation in the static r…
rolfbjarne Jan 25, 2023
c9e9875
[registrar] Make some API from the registrar public so that the manag…
rolfbjarne Jan 25, 2023
03385da
[src] Fix comparison between signed and unsigned int.
rolfbjarne Jan 25, 2023
c2ce5fd
[static registrar] Add support for generating block syntax in Objecti…
May 5, 2023
0a89e5e
[static registrar] Move token reference creation a little bit later.
May 5, 2023
6331f13
[tools] Move code to compute block signatures to the static registrar.
May 5, 2023
d8f9434
[registrar] Add an HasCustomAttribute overload that returns the found…
May 5, 2023
01e11ca
[dotnet-linker] Add extension methods for making IL emission easier w…
May 5, 2023
1453a71
[dotnet-linker] Add a helper class for keeping track of methods and t…
May 5, 2023
561011c
[registrar] Refactor code to determine if a method is a property acce…
May 5, 2023
2fde3be
[dotnet-linker] Remove trimmed API from the registered types before g…
May 5, 2023
da579c4
[static registrar] Refactor code to make it easier to reuse code late…
May 8, 2023
645f76e
[src] Refactor Class.ResolveToken to take the assembly as a parameter.
Apr 28, 2023
b6d71db
[runtime] Add an API to look up the native symbol for an [UnmanagedCa…
rolfbjarne Jan 25, 2023
09a74ad
[tools] Add a managed static registrar. Fixes #17324.
May 9, 2023
b51cfbc
[static registrar] Implement support for calling the generated Unmana…
May 5, 2023
5b9fe79
[src] Add helper methods for the managed static registrar
May 5, 2023
5d03d70
[registrar] We might link the [Protocol] attribute away, so store it …
May 5, 2023
047a587
[src] Track selector and method better to provide more helpful error …
rolfbjarne Jan 25, 2023
444d8aa
[tests] Improve the current registrar detection in the tests
rolfbjarne Jan 25, 2023
14abd21
[tests] Add a check for the managed static registrar
rolfbjarne Jan 25, 2023
ffa3c04
[tests] Update to work with the managed static registrar
rolfbjarne Jan 25, 2023
368bd68
[monotouch-test] Use MidiThruConnectionEndpoint instead of MidiCIDevi…
rolfbjarne May 8, 2023
7fbd48c
[tests] Adjust to cope with slightly different errors reported when u…
rolfbjarne Apr 26, 2023
cee17c0
[linker] Don't optimize calls to BlockLiteral.SetupBlock in BlockLite…
rolfbjarne May 11, 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.

## 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 tha 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 trimmed 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 @@ -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).
* 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