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

[NativeAOT] support calling UnmanagedCallersOnly functions before managed main() #77957

Closed
Tracked by #77472
AustinWise opened this issue Nov 6, 2022 · 21 comments
Closed
Tracked by #77472
Assignees
Milestone

Comments

@AustinWise
Copy link
Contributor

AustinWise commented Nov 6, 2022

Problem statement

Some existing applications use the CoreCLR hosting APIs to call managed functions before calling the main method of an assembly. A specific example is xamarin-macios. It uses coreclr_create_delegate and reverse P/Invoke to call some functions before using coreclr_execute_assembly to execute an assembly's main function.

While trying to replicate the CoreCLR hosting API does not make sense for NativeAOT, the ability to call managed functions before main() would be useful.

Implementation Ideas

Currently ILC will generate one of two entry points: __managed__Main for executables and __managed__Startup for libraries. If both were generated, a hosting application could call functions in the following order:

  1. Initialize the NativeAOT runtime using __managed__Startup
  2. Call UnmanagedCallersOnly functions
  3. Manuplicate argc and argv if desired.
  4. Call __managed__Main to run the application.

When generating both entry points, __managed__Main should not run library or module initializers.

Ass a proof of concept, see this example adding support to ILC in this commit and then using those entry points with a custom bootstrapper to emulate enough of the CoreCLR hosting API to make xamarin-macios work.

Alternatives Considered

Conceivably hosts like xamarin-macios could use module constructors to run code before the main method, as NativeAOT eagerly runs these at startup. The only missing feature when using this approach is the ability to manipulate command line arguments before the main method starts.

Open Questions

  • Should this capability only work when statically-linking the NativeAOT runtime? Should it also work when compiling the runtime and user EXE as shared library?
  • What should the command line arguments for ILC look like
  • How should the bootstrapper change to support these entry points.
  • How should this feature be exposed in MSBuild to users

Related issues

@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Nov 6, 2022
@jkotas
Copy link
Member

jkotas commented Nov 7, 2022

This sounds like an AOT variant of startup hook (https://github.com/dotnet/runtime/blob/main/docs/design/features/host-startup-hook.md).

@AustinWise
Copy link
Contributor Author

This sounds like an AOT variant of startup hook (https://github.com/dotnet/runtime/blob/main/docs/design/features/host-startup-hook.md).

Basically, yes. Something like a start up hook won't let you manipulate the command line arguments before the managed main, but that's not required for the xamarin-macios use case I had in mind.

@MichalStrehovsky
Copy link
Member

Basically, yes. Something like a start up hook won't let you manipulate the command line arguments before the managed main, but that's not required for the xamarin-macios use case I had in mind.

If we don't need the command line manipulation, can we do this using the module constructor approach? The only problem with that is that C# doesn't allow ordering the module constructor fragments, but if we do it from MSBuild we can have this to be the first file and that probably means it will be the first method to be called from the module constructor too.

@MichalStrehovsky MichalStrehovsky added this to the Future milestone Nov 7, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Nov 7, 2022
@ivanpovazan
Copy link
Member

I was wondering whether it would be possible to have:

  1. A xamarin app, which wraps its Main with UnmanagedCallersOnly method
  2. Compile it as a static library with NativeAOT
  3. Adjust the xamarin startup (generated bootstrap main or xamarin_main) to invoke NativeAOT_StaticInitialization() provided by libbootstrapperdll.a, and then the mentioned UnmanagedCallersOnly wrapper of the managed app
  4. Adjust the xamarin build to invoke ilc (step 2.) and also include the app's static library and NativeAOT bootstrapper when linking the native executable

Wouldn't this work?

@jkotas
Copy link
Member

jkotas commented Nov 7, 2022

Yes, it is possible to achieve this by custom build steps that modify the app, e.g. by injecting a module constructor or wrapping the Main method.

Again, it looks similar to startup hook. It was always possible to approximate what startup hook does by writing a custom host or by modifying the app. We had multiple customers building complex fragile solutions to solve the problem. We have decided to introduce the startup hook feature to simplify the life of these customers (and ours too since we would often end up debugging issues in the complex fragile solutions).

So, the question is whether we believe that injecting code before Main is common enough scenario for native AOT to introduce a feature for it.

@MichalStrehovsky
Copy link
Member

So, the question is whether we believe that injecting code before Main is common enough scenario for native AOT to introduce a feature for it.

What would the requirements look like? Startup hook allows introducing code before Main after the app was fully built. So e.g. one can use it to introduce new code into even dotnet.

A Native AOT equivalent would be to dlopen/dlsym (LoadLibrary/GetProcAddress) whatever was in the environment variable and p/invoke into it.

Or we could say we only want a compile-time startup hook. This is a lot less powerful than startup hooks. I have a hard time seeing difference from just adding a module initializer.

Neither of those would allow modifying the command line arguments though which looks like a requirement for Xamarin (and definitely a one-off that is hard to imagine a general purpose solution for).

@jkotas
Copy link
Member

jkotas commented Nov 9, 2022

We could say we only want a compile-time startup hook. This is a lot less powerful than startup hooks. I have a hard time seeing difference from just adding a module initializer.

Module initializer requires appending a new .cs file to the app sources. It is certainly doable. I am not sure how hard it is to hide it so that it does not show as part of the solution in the GUI.

@MichalStrehovsky
Copy link
Member

We could say we only want a compile-time startup hook. This is a lot less powerful than startup hooks. I have a hard time seeing difference from just adding a module initializer.

Module initializer requires appending a new .cs file to the app sources. It is certainly doable. I am not sure how hard it is to hide it so that it does not show as part of the solution in the GUI.

The default targets already generate two such files into obj for a hello world: a .NETCoreApp,Version=v7.0.AssemblyAttributes.cs and Foo.AssemblyInfo.cs so I would say there's enough of a precedent. Adding things to <Compile> within a <Target> is probably the thing that does the trick. This would work for all of our runtimes without any further work.

@AustinWise
Copy link
Contributor Author

I think the idea of injecting a module constructor into the main program from the SDK would work fine.

@ivanpovazan
Copy link
Member

Reopening this issue as it became a requirement of a broader work item.

@AustinWise thanks for your suggestions and contributions! Would you be interested in contributing in resolving this issue?

@ivanpovazan ivanpovazan reopened this Jan 20, 2023
@jkotas
Copy link
Member

jkotas commented Jan 20, 2023

@ivanpovazan If we go with injecting a module constructor into the main program from the SDK, should this issue be tracked in xamarin-macios repo instead where the relevant SDK lives?

@ivanpovazan
Copy link
Member

@jkotas if I am not mistaken, I believe the requirement is more general.

Example: someone has a desktop managed application compiled as a static library with NativeAOT, and wants to integrated it into a native application.
This would also require the UnmanagedCallersOnly wrapper to be in-place.

For this reason, I thought the change belongs to: https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets

What do you think?

@jkotas
Copy link
Member

jkotas commented Jan 20, 2023

This should not be Native-AOT specific change. It should be equally applicable to all runtimes.

@jkotas
Copy link
Member

jkotas commented Jan 20, 2023

Note that the issue title does not match the design that we have converged to: #77957 (comment)

@ivanpovazan
Copy link
Member

ivanpovazan commented Jan 23, 2023

@jkotas, thank you.
I might misunderstood the intent and requirement outlined in the issue.
For that reason, I am currently investigating with @rolfbjarne whether injecting a module constructor into the main program is necessary to enable NativeAOT with Xamarin.

@ivanpovazan ivanpovazan self-assigned this Jan 23, 2023
@lambdageek
Copy link
Member

lambdageek commented Jan 24, 2023

Note also that we recently enabled startup hooks for mobile platforms (running on Mono #80391). The StartupHookProvider is basically portable code - could be ported over to NativeAOT.

It seems like which approach we use depends on how we want to think about the ios app. If we compile it as a static library, then the startup sequence is: Microsoft.iOS starts in native -> calls some UnmanagedCallersOnly methods (a Main wrapper) in Microsoft.iOS and then -> calls the user's Main. If we make an executable, we could instead do: NativeAOT native main starts -> executes startup hooks which start Microsoft.iOS; Native AOT native main -> calls managed Main which runs in an environment configured by the startup hook.

@ivanpovazan
Copy link
Member

After internal discussions, it has been concluded that there will be no need for injecting module constructor into the main program, as Xamarin will implement a new managed-to-native bridge for NativeAOT. The startup code will be responsible of invoking any required bootstrap and/or initialization managed code in the correct order, by utilizing symbols exposed via UnmanagedCallersOnlyAttribute.

A pseudocode example of a native startup sequence would be:

call NativeAOT_StaticInitialization
call ObjCRuntime.Runtime::Initialize <- call into managed
call MyApplication::Main <- call into managed

where ObjCRuntime.Runtime::Initialize and MyApplication::Main methods will have to be exposed via UnmanagedCallersOnlyAttribute.

The progress of the work on Xamarin side is tracked here: xamarin/xamarin-macios#17339


For this reason I am closing the issue to avoid any further confusion.

@jkotas
Copy link
Member

jkotas commented Jan 24, 2023

the startup hook

We do not plan to support startup hooks for native AOT compiled apps.

Startup hooks are meant to be used for dynamic non-trimmed scenarios where a new component is injected into the app, e.g. to light-up a new diagnostic capability that the app is not aware of.

@lambdageek
Copy link
Member

the startup hook

We do not plan to support startup hooks for native AOT compiled apps.

Startup hooks are meant to be used for dynamic non-trimmed scenarios where a new component is injected into the app, e.g. to light-up a new diagnostic capability that the app is not aware of.

I think providing capabilities of which the app is not aware and being dynamic and non-trimmable are separate issues. Startup hooks could be used in static, trimmed, scenarios on mobile.

Providing a diagnostic capability of which the app is not aware can be done at publish time for debug configurations, for example. The startup hook is in the runtimeconfig.template.json, it's a ProjectReference injected by the SDK, the trimmer can be fully aware of it (or indeed the trimmer can be disabled in a Debug build). The important thing is to get the hook assembly into the app bundle and initialized before the user's code begins executing.

@rolfbjarne
Copy link
Member

It seems like which approach we use depends on how we want to think about the ios app. If we compile it as a static library, then the startup sequence is: Microsoft.iOS starts in native -> calls some UnmanagedCallersOnly methods (a Main wrapper) in Microsoft.iOS and then -> calls the user's Main. If we make an executable, we could instead do: NativeAOT native main starts -> executes startup hooks which start Microsoft.iOS; Native AOT native main -> calls managed Main which runs in an environment configured by the startup hook.

Things get a bit more complicated when it comes to iOS App Extensions. There are very much like normal app bundles (they have a native executable for instance), but the executable project doesn't have a native main method. Instead Apple instructs the native linker to use a different symbol as the native entry point (the exact name depends on which type of app extension it is), which Apple provides in a static library and is linked into the main executable. (The actual entry points into the app (extension) are UI-based, where a certain UI view might be instantiated for instance).

Our way of dealing with this is to provide a native main function, tell the native linker to use that function as the entry point, and then we call Apple's custom app-extension-specific main function once we're done initializing our own code (instead of calling the managed Main method).

To summarize I think this scenario can be simplified as "we don't always call managed Main, but another native function instead". It seems this is harder/more work to implement on the NativeAOT side if NativeAOT creates an executable instead of a static library.

I'm also favoring the first approach (static library) because that matches how we're doing things with both Mono and CoreCLR, and having a similar design for all options makes the code easier to understand and maintain on our side.

Also worthy of note is that iOS App Extensions might be one of the more interesting targets for NativeAOT, because they can be severely constrained compared to normal iOS apps (max disk space requirements, max memory requirements, etc.)

MichalStrehovsky pushed a commit that referenced this issue Feb 15, 2023
)

This PR adds a new flag to ILC, `--splitinit` . The flag splits the initialization that is normally done for executable into two parts. The `__managed__Startup` function runs classlib and module initializers. The `__managed__Main` function performs the remaining initialization and executes the entry-point of the managed application.

The use case for this is to allow calling `UnamanagedCallersOnly` functions before the managed `main` function. Running on iOS is the first use case for this feature (#80905).

It was not clear to me how to fit this into the larger NativeAot build system. Should this be thought of as "a static library that also has a `__managed__Main` compiled in" or as "an executable that splits its initialization into two parts"? So I added support to ILC for both cases: compiling with the `--nativelib` flag and without it.

Lastly, I added some build integration the "an executable that splits its initialization into two parts" case, along with test. The idea is the user sets the `CustomNativeMain` property in their project. They are then responsible for providing a `NativeLibrary` item pointing to an object file containing their native `main` function. At runtime, when they call their first managed function, NativeAOT initialization will run. This includes calling `__managed__Main`, so the user cannot forget to initialize the runtime.

Related issues: #81097 #77957
@ghost ghost locked as resolved and limited conversation to collaborators Feb 24, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants