Skip to content

Commit 0bc12c8

Browse files
authored
Add a managed static registrar. Fixes #17324. (#18268)
Add a new version of the static registrar (called the managed static registrar), which most notably doesn't use metadata tokens (because NativeAOT doesn't support metadata tokens). In addition, the new registrar also takes advantage of new features in both C# and the runtime, in order to be more performant. I won't go into detail about everything here, because it would be rather long, but I've added documentation for the new registrar (the first commit, so start reviewing there). Fixes #17324.
2 parents 6e216f0 + 65590ba commit 0bc12c8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+5423
-575
lines changed

docs/managed-static-registrar.md

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# Managed static registrar
2+
3+
The managed static registrar is a variation of the static registrar where we
4+
don't use features the NativeAOT compiler doesn't support (most notably
5+
metadata tokens).
6+
7+
It also takes advantage of new features in C# and managed code since the
8+
original static registrar code was written - in particular it tries to do as
9+
much as possible in managed code instead of native code, as well as various
10+
other performance improvements. The actual performance characteristics
11+
compared to the original static registrar will vary between the specific
12+
exported method signatures, but in general it's expected that method calls
13+
from native code to managed code will be faster.
14+
15+
In order to make the managed static registrar easily testable and debuggable,
16+
it's also implemented for the other runtimes as well (Mono and CoreCLR as
17+
well), as well as when not using AOT in any form.
18+
19+
## Design
20+
21+
### Exported methods
22+
23+
For each method exported to Objective-C, the managed static registrar will
24+
generate a managed method we'll call directly from native code, and which does
25+
all the marshalling.
26+
27+
This method will have the [UnmanagedCallersOnly] attribute, so that it doesn't
28+
need any additional marshalling from the managed runtime - which makes it
29+
possible to obtain a native function pointer for it. It will also have a
30+
native entry point, which means that for AOT we can just directly call it from
31+
the generated Objective-C code.
32+
33+
Given the following method:
34+
35+
```csharp
36+
class AppDelegate : NSObject, IUIApplicationDelegate {
37+
// this method is written by the app developer
38+
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
39+
{
40+
// ...
41+
}
42+
}
43+
```
44+
45+
The managed static registrar will add the following method to the `AppDelegate` class:
46+
47+
```csharp
48+
class AppDelegate {
49+
[UnmanagedCallersOnly (EntryPoint = "__registrar__uiapplicationdelegate_didFinishLaunching")]
50+
static byte __registrar__DidFinishLaunchingWithOptions (IntPtr handle, IntPtr selector, IntPtr p0, IntPtr p1)
51+
{
52+
var obj = Runtime.GetNSObject (handle);
53+
var p0Obj = (UIApplication) Runtime.GetNSObject (p0);
54+
var p1Obj = (NSDictionary) Runtime.GetNSObject (p1);
55+
var rv = obj.DidFinishLaunchingWithOptions (p0Obj, p1Obj);
56+
return rv ? (byte) 1 : (byte) 0;
57+
}
58+
}
59+
```
60+
61+
and the generated Objective-C code will look something like this:
62+
63+
```objective-c
64+
extern BOOL __registrar__uiapplicationdelegate_init (AppDelegate self, SEL _cmd, UIApplication* p0, NSDictionary* p1);
65+
66+
@interface AppDelegate : NSObject<UIApplicationDelegate, UIApplicationDelegate> {
67+
}
68+
-(BOOL) application:(UIApplication *)p0 didFinishLaunchingWithOptions:(NSDictionary *)p1;
69+
@end
70+
@implementation AppDelegate {
71+
}
72+
-(BOOL) application:(UIApplication *)p0 didFinishLaunchingWithOptions:(NSDictionary *)p1
73+
{
74+
return __registrar__uiapplicationdelegate_didFinishLaunching (self, _cmd, p0, p1);
75+
}
76+
@end
77+
```
78+
79+
Note: the actual code is somewhat more complex in order to properly support
80+
managed exceptions and a few other corner cases.
81+
82+
### Type mapping
83+
84+
The runtime needs to quickly and efficiently do lookups between an Objective-C
85+
type and the corresponding managed type. In order to support this, the managed
86+
static registrar will add lookup tables in each assembly. The managed static
87+
registrar will create a numeric ID for each managed type, which is then
88+
emitted into the generated Objective-C code, and which we can use to look up
89+
the corresponding managed type. There is also a table in Objective-C that maps
90+
between the numeric ID and the corresponding Objective-C type.
91+
92+
We also need to be able to find the wrapper type for interfaces representing
93+
Objective-C protocols - this is accomplished by generating a table in
94+
unmanaged code that maps the ID for the interface to the ID for the wrapper
95+
type.
96+
97+
This is all supported by the `ObjCRuntime.IManagedRegistrar.LookupTypeId` and
98+
`ObjCRuntime.IManagedRegistrar.LookupType` methods.
99+
100+
Note that in many ways the type ID is similar to the metadata token for a type
101+
(and is sometimes referred to as such in the code, especially code that
102+
already existed before the managed static registrar was implemented).
103+
104+
### Method mapping
105+
106+
When AOT-compiling code, the generated Objective-C code can call the entry
107+
point for the UnmanagedCallersOnly trampoline directly (the AOT compiler will
108+
emit a native symbol with the name of the entry point).
109+
110+
However, when not AOT-compiling code, the generated Objective-C code needs to
111+
find the function pointer for the UnmanagedCallersOnly methods. This is
112+
implemented using another lookup table in managed code.
113+
114+
For technical reasons, this is implemented using multiple levels of functions if
115+
there is a significant number of UnmanagedCallersOnly methods. As it seems
116+
that the JIT will compile the target for every function pointer in a method,
117+
even if the function pointer isn't loaded at runtime. This means that if
118+
there are 1.000 methods in the lookup table and if the lookup was
119+
implemented in a single function, the JIT will have to compile all
120+
the 1.000 methods the first time the lookup method is called, even
121+
if the lookup method will eventually just find a single callback.
122+
123+
This might be easier to describe with some code.
124+
125+
Instead of this:
126+
127+
```csharp
128+
class __Registrar_Callbacks__ {
129+
IntPtr LookupUnmanagedFunction (int id)
130+
{
131+
switch (id) {
132+
case 0: return (IntPtr) (delegate* unmanaged<void>) &Callback0;
133+
case 1: return (IntPtr) (delegate* unmanaged<void>) &Callback1;
134+
...
135+
case 999: return (IntPtr) (delegate* unmanaged<void>) &Callback999;
136+
}
137+
return (IntPtr) -1);
138+
}
139+
}
140+
```
141+
142+
we do this instead:
143+
144+
```csharp
145+
class __Registrar_Callbacks__ {
146+
IntPtr LookupUnmanagedFunction (int id)
147+
{
148+
if (id < 100)
149+
return LookupUnmanagedFunction_0 (id);
150+
if (id < 200)
151+
return LookupUnmanagedFunction_1 (id);
152+
...
153+
if (id < 1000)
154+
LookupUnmanagedFunction_9 (id);
155+
return (IntPtr) -1;
156+
}
157+
158+
IntPtr LookupUnmanagedFunction_0 (int id)
159+
{
160+
switch (id) {
161+
case 0: return (IntPtr) (delegate* unmanaged<void>) &Callback0;
162+
case 1: return (IntPtr) (delegate* unmanaged<void>) &Callback1;
163+
/// ...
164+
case 9: return (IntPtr) (delegate* unmanaged<void>) &Callback9;
165+
}
166+
return (IntPtr) -1;
167+
}
168+
169+
170+
IntPtr LookupUnmanagedFunction_1 (int id)
171+
{
172+
switch (id) {
173+
case 10: return (IntPtr) (delegate* unmanaged<void>) &Callback10;
174+
case 11: return (IntPtr) (delegate* unmanaged<void>) &Callback11;
175+
/// ...
176+
case 19: return (IntPtr) (delegate* unmanaged<void>) &Callback19;
177+
}
178+
return (IntPtr) -1;
179+
}
180+
}
181+
```
182+
183+
184+
### Generation
185+
186+
All the generated IL is done in two separate custom linker steps. The first
187+
one, ManagedRegistrarStep, will generate the UnmanagedCallersOnly trampolines
188+
for every method exported to Objective-C. This happens before the trimmer has
189+
done any work (i.e. before marking), because the generated code will cause
190+
more code to be marked (and this way we don't have to replicate what the
191+
trimmer does when it traverses IL and metadata to figure out what else to
192+
mark).
193+
194+
The trimmer will then trim away any UnmanagedCallersOnly trampoline that's no
195+
longer needed because the target method has been trimmed away.
196+
197+
On the other hand, the lookup tables for the type mapping are generated after
198+
trimming, because we only want to add types that aren't trimmed away to the
199+
lookup tables (otherwise we'd end up causing all those types to be kept).
200+
201+
## Interpreter / JIT
202+
203+
When not using the AOT compiler, we need to look up the native entry points
204+
for UnmanagedCallersOnly methods at runtime. In order to support this, the
205+
managed static registrar will add lookup tables in each assembly. The managed
206+
static registrar will create a numeric ID for each UnmanagedCallersOnly
207+
method, which is then emitted into the generated Objective-C code, and which
208+
we can use to look up the managed UnmanagedCallersOnly method at runtime (in
209+
the lookup table).
210+
211+
This is the `ObjCRuntime.IManagedRegistrar.LookupUnmanagedFunction` method.
212+
213+
## Performance
214+
215+
Preliminary testing shows the following:
216+
217+
### macOS
218+
219+
Calling an exported managed method from Objective-C is 3-6x faster for simple method signatures.
220+
221+
### Mac Catalyst
222+
223+
Calling an exported managed method from Objective-C is 30-50% faster for simple method signatures.
224+
225+
## References
226+
227+
* https://github.com/dotnet/runtime/issues/80912

dotnet/targets/Xamarin.Shared.Sdk.targets

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,10 @@
557557
<_ExtraTrimmerArgs Condition="('$(_PlatformName)' == 'iOS' Or '$(_PlatformName)' == 'tvOS') And '$(_SdkIsSimulator)' == 'true'">$(_ExtraTrimmerArgs) --feature ObjCRuntime.Runtime.Arch.IsSimulator true</_ExtraTrimmerArgs>
558558
<_ExtraTrimmerArgs Condition="('$(_PlatformName)' == 'iOS' Or '$(_PlatformName)' == 'tvOS') And '$(_SdkIsSimulator)' != 'true'">$(_ExtraTrimmerArgs) --feature ObjCRuntime.Runtime.Arch.IsSimulator false</_ExtraTrimmerArgs>
559559

560+
<!-- Set managed static registrar value -->
561+
<_ExtraTrimmerArgs Condition="'$(Registrar)' == 'managed-static'">$(_ExtraTrimmerArgs) --feature ObjCRuntime.Runtime.IsManagedStaticRegistrar true</_ExtraTrimmerArgs>
562+
<_ExtraTrimmerArgs Condition="'$(Registrar)' != 'managed-static'">$(_ExtraTrimmerArgs) --feature ObjCRuntime.Runtime.IsManagedStaticRegistrar false</_ExtraTrimmerArgs>
563+
560564
<!-- Enable serialization discovery. Ref: https://github.com/xamarin/xamarin-macios/issues/15676 -->
561565
<_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --enable-serialization-discovery</_ExtraTrimmerArgs>
562566

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

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

609+
<!--
610+
pre-sweep custom steps
611+
-->
612+
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="SweepStep" Type="Xamarin.Linker.ManagedRegistrarLookupTablesStep" Condition="'$(Registrar)' == 'managed-static'" />
613+
604614
<!--
605615
post-sweep custom steps
606616
-->

runtime/delegates.t4

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,14 @@
667667
) {
668668
WrappedManagedFunction = "InvokeConformsToProtocol",
669669
},
670+
671+
new XDelegate ("void *", "IntPtr", "xamarin_lookup_unmanaged_function",
672+
"const char *", "IntPtr", "assembly",
673+
"const char *", "IntPtr", "symbol",
674+
"int32_t", "int", "id"
675+
) {
676+
WrappedManagedFunction = "LookupUnmanagedFunction",
677+
},
670678
};
671679
delegates.CalculateLengths ();
672680
#><#+

runtime/runtime.m

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@
136136

137137
enum InitializationFlags : int {
138138
InitializationFlagsIsPartialStaticRegistrar = 0x01,
139-
/* unused = 0x02,*/
139+
InitializationFlagsIsManagedStaticRegistrar = 0x02,
140140
/* unused = 0x04,*/
141141
/* unused = 0x08,*/
142142
InitializationFlagsIsSimulator = 0x10,
@@ -2736,6 +2736,30 @@ -(void) xamarinSetFlags: (enum XamarinGCHandleFlags) flags;
27362736
[message release];
27372737
}
27382738

2739+
void
2740+
xamarin_registrar_dlsym (void **function_pointer, const char *assembly, const char *symbol, int32_t id)
2741+
{
2742+
if (*function_pointer != NULL)
2743+
return;
2744+
2745+
*function_pointer = dlsym (RTLD_MAIN_ONLY, symbol);
2746+
if (*function_pointer != NULL)
2747+
return;
2748+
2749+
GCHandle exception_gchandle = INVALID_GCHANDLE;
2750+
*function_pointer = xamarin_lookup_unmanaged_function (assembly, symbol, id, &exception_gchandle);
2751+
if (*function_pointer != NULL)
2752+
return;
2753+
2754+
if (exception_gchandle != INVALID_GCHANDLE)
2755+
xamarin_process_managed_exception_gchandle (exception_gchandle);
2756+
2757+
// This shouldn't really happen
2758+
NSString *msg = [NSString stringWithFormat: @"Unable to load the symbol '%s' to call managed code: %@", symbol, xamarin_print_all_exceptions (exception_gchandle)];
2759+
NSLog (@"%@", msg);
2760+
@throw [[NSException alloc] initWithName: @"SymbolNotFoundException" reason: msg userInfo: NULL];
2761+
}
2762+
27392763
/*
27402764
* File/resource lookup for assemblies
27412765
*
@@ -3195,6 +3219,16 @@ -(enum XamarinGCHandleFlags) xamarinGetFlags
31953219
return xamarin_debug_mode;
31963220
}
31973221

3222+
void
3223+
xamarin_set_is_managed_static_registrar (bool value)
3224+
{
3225+
if (value) {
3226+
options.flags = (InitializationFlags) (options.flags | InitializationFlagsIsManagedStaticRegistrar);
3227+
} else {
3228+
options.flags = (InitializationFlags) (options.flags & ~InitializationFlagsIsManagedStaticRegistrar);
3229+
}
3230+
}
3231+
31983232
bool
31993233
xamarin_is_managed_exception_marshaling_disabled ()
32003234
{

runtime/xamarin/runtime.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ void xamarin_check_objc_type (id obj, Class expected_class, SEL sel, id self,
254254
#endif
255255

256256
void xamarin_set_gc_pump_enabled (bool value);
257+
void xamarin_set_is_managed_static_registrar (bool value);
257258

258259
void xamarin_process_nsexception (NSException *exc);
259260
void xamarin_process_nsexception_using_mode (NSException *ns_exception, bool throwManagedAsDefault, GCHandle *output_exception);
@@ -295,6 +296,15 @@ void xamarin_printf (const char *format, ...);
295296
void xamarin_vprintf (const char *format, va_list args);
296297
void xamarin_install_log_callbacks ();
297298

299+
/*
300+
* Looks up a native function pointer for a managed [UnmanagedCallersOnly] method.
301+
* function_pointer: the return value, lookup will only be performed if this points to NULL.
302+
* assembly: the assembly to look in. Might be NULL if the app was not built with support for loading additional assemblies at runtime.
303+
* symbol: the symbol to look up. Can be NULL to save space (this value isn't used except in error messages).
304+
* id: a numerical id for faster lookup (than doing string comparisons on the symbol name).
305+
*/
306+
void xamarin_registrar_dlsym (void **function_pointer, const char *assembly, const char *symbol, int32_t id);
307+
298308
/*
299309
* Wrapper GCHandle functions that takes pointer sized handles instead of ints,
300310
* so that we can adapt our code incrementally to use pointers instead of ints

src/Foundation/NSArray.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,19 @@ static public T [] ArrayFromHandle<T> (NativeHandle handle) where T : class, INa
295295
return ret;
296296
}
297297

298+
static Array ArrayFromHandle (NativeHandle handle, Type elementType)
299+
{
300+
if (handle == NativeHandle.Zero)
301+
return null;
302+
303+
var c = (int) GetCount (handle);
304+
var rv = Array.CreateInstance (elementType, c);
305+
for (int i = 0; i < c; i++) {
306+
rv.SetValue (UnsafeGetItem (handle, (nuint) i, elementType), i);
307+
}
308+
return rv;
309+
}
310+
298311
static public T [] EnumsFromHandle<T> (NativeHandle handle) where T : struct, IConvertible
299312
{
300313
if (handle == NativeHandle.Zero)
@@ -395,6 +408,18 @@ static T UnsafeGetItem<T> (NativeHandle handle, nuint index) where T : class, IN
395408
return Runtime.GetINativeObject<T> (val, false);
396409
}
397410

411+
static object UnsafeGetItem (NativeHandle handle, nuint index, Type type)
412+
{
413+
var val = GetAtIndex (handle, index);
414+
// A native code could return NSArray with NSNull.Null elements
415+
// and they should be valid for things like T : NSDate so we handle
416+
// them as just null values inside the array
417+
if (val == NSNull.Null.Handle)
418+
return null;
419+
420+
return Runtime.GetINativeObject (val, false, type);
421+
}
422+
398423
// can return an INativeObject or an NSObject
399424
public T GetItem<T> (nuint index) where T : class, INativeObject
400425
{

0 commit comments

Comments
 (0)