8000 Proposal for allowing "get delegate" functionality on application host context by vitek-karas · Pull Request #36990 · dotnet/runtime · GitHub
[go: up one dir, main page]

Skip to content

Proposal for allowing "get delegate" functionality on application host context #36990

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

Merged
merged 7 commits into from
Jun 5, 2020

Conversation

vitek-karas
Copy link
Member

Removes the limitation of hostfxr_get_runtime_delegate to make it work on app host context as well.

Defines a new runtime delegate hdt_get_function_pointer which is very similar to hdt_load_assembly_and_get_function_pointer but operates on default load context only and doesn't allow loading new assemblies.

This also allows passing nullptr for the host context parameter of hostfxr_get_runtime_delegate which would make it work on the first host context (just like this is already possible for hostfxr_get_runtime_property_value for example).

@ghost
Copy link
ghost commented May 25, 2020

Tagging subscribers to this area: @vitek-karas, @swaroop-sridhar
Notify danmosemsft if you want to be subscribed.

@vitek-karas
Copy link
Member Author

@vitek-karas
Copy link
Member Author

I want to note that I made certain design choices here which we should discuss as part of this PR:

Get a native function pointer to method from default load context

In the PR I propose we do this by adding a new runtime helper/delegate hdt_get_function_pointer which would always operate on default load context only. The reasoning is:

  • Pros: clearly separate from already existing hdt_load_assembly_and_get_function_pointer, has different parameters to make it clear that no assemblies can be added to the runtime this way
  • Cons: Implements relatively similar concept as hdt_load_assembly_and_get_function_pointer so may cause confusion.

The alternative design is to not introduce a new runtime helper and add this capability to the hdt_load_assembly_and_get_function_pointer. This could be done by:

  • Allowing nullptr for the assembly_path parameter - in which case the behavior would identical to the new runtime helper. The downside of this approach is that the assembly_path parameter would control two things, which assembly to load, and which load context to operate on - pretty confusing.
  • Using the reserved parameter and define a new option to operate on default load context. On top of that allow the assembly_path to be nullptr. No assembly path would not be allowed without asking for default load context (as that doesn't make sense). This would also enable a scenario where an external plugin (assembly) is loaded into the default load context. This would require us to define assembly resolution strategy (which is slightly different for default ALC then for secondary ALCs). The main downside of this is that not all combinations are allowed and that the assembly resolution logic would very likely behave slightly differently on default ALC versus secondary ALC (not counting problems with version conflicts and so on - just like any other loading of assemblies into the default ALC).

Allowing nullptr for the context parameter

We could omit this feature. Having this feature basically allows native code to load a managed plugin into already operating runtime/app. When done from managed code we don't perform any framework resolution either, one can simply load assemblies via ALCs. But without this feature native code can only do so with a .runtimeconfig.json which forces framework resolution.

I don't have a strong opinion on this one and if we think we don't need it, or don't want it we can remove this (it is possible to allow in a later release).

Note: that the opposite to this is a feature where AssemblyDependencyResolver would allow to perform framework resolution - which has also been asked for. Which would in turn make it very similar to the existing native hosting APIs for loading managed components. Both cases are desirable in some scenarios, so I don't think having both enabled from both managed and native is "bad".

@vitek-karas
Copy link
Member Author

Note on complexity: I already prototyped this change to allow hostfxr_get_runtime_delagate on any context, including the nullptr one. Such change is very simple and works well. That said I didn't do any concurrency testing, but there should be no issues with that.

Adding the new runtime helper to operate on default ALC only also feels pretty straightforward (but I didn't prototype that one).

@vitek-karas
Copy link
Member Author

One final note: The proposal is written as if we deliver this in .NET 5, which is definitely not guaranteed. Depending on who implements this and when we should adapt the doc accordingly.

Copy link
Contributor
@rseanhall rseanhall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this proposal covers my requirements.

@@ -209,6 +211,7 @@ The function returns specific return code for the first initialized host context
* In both cases only the properties specified by the new runtime config will be reported on the host context. This is to allow the native host to decide in the "warning" case if it's OK to let the component run or not.
* In both cases the returned host context can still be used to get a runtime delegate, the properties from the new runtime config will be ignored (as there's no way to modify those in the runtime).

The specified `.runtimeconfig.json` must be for a framework dependent component. That is it must specify at least one shared framework in its `frameworks` section. Self-contained components are not supported.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of my questions about this design basically boil down to this: why should hostfxr_initialize_for_runtime_config still block on an SCD .runtimeconfig.json since the user can now just use hostfxr_initialize_for_dotnet_command_line instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind hostfxr_initialize_for_runtime_config is targeted at components (so things like plugins). It's usefulness for apps is questionable due to the fact that it doesn't populate TPA (the default ALC) with application code, only frameworks.

On top of that "self-contained component" is not a thing in .NET Core (yet). There's no SDK support for it. We only allow one runtime in process as well, which complicates this as well.

If we were to allow SCD in hostfxr_initialize_for_runtime_config it would have to be either a very limited experience (cumbersome build, only one such component in process, hard to explain, ...) or we would have to deliver lot of new functionality (SDK support, probably at least some basic VS support, open the question of multiple runtimes in process which ignoring some technical difficulties in the runtime would cause lot of problems for diagnostics, ...).

Combined with the fact that the scenario you describe (along with several other users asking for something similar) is somewhere in between app and component (as you correctly pointed out and I didn't realize at the beginning, your scenario is much closure to an app in a way).

I've also seen a few asks on allowing the "get delegate" functionality on pure apps (no component involved), even ones started directly as .NET Core apps (no native host at the beginning).

In theory it's possible to use hostfxr_initialize_for_dotnet_command_line on "components", but I view that as "bending the system" - quite a bit in fact. And still, if I would want SCD, then it has to be an app, there's no other supported way to actually build it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In .NET 5, the distinction between classlib and app is going away. There is no netstandard for .NET 5, there's only the net5.0 TFM. It was always possible to build a component targeting netcoreappX.Y. So I don't think that part of the argument is valid.

What I'm trying to point out here is that under this design, hostfxr_get_runtime_delegate can now be called from a context that didn't perform this SCD check. Which means all of the existing delegate types (which are all targeted to the component scenario) can now be called against an SCD component. If the artificial block in hostfxr_initialize_for_runtime_config is kept in, people are just going to get frustrated and go around it anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a TFM which targets a specific runtime versus netstandard is not the same thing as treating classlib and app the same way. As you note it was always possible to target classlib for a specific runtime, that's not the limitation. The limitation is that SDK simply doesn't officially support building SCD classlib projects. In a way even dotnet publish is sort of weird on a classlib (and has several really weird behaviors). All of this is mostly because nobody went and made sure that SDK has this scenario cleaned up. Introducing support for this in the hosting layer feels wrong if we don't have a good story how to build such a thing.

You're right that we might need to block one of the other supported delegates from this new scenario. But most of them should work. There's a separate question if we should support them there - as COM/WinRT/IJW should never run into the new scenario as those entry points will always go through the hostfxr_initialize_for_runtime_config. So maybe it would be better to block those cases (just to avoid having to support weird configurations into the future).

I don't think the block in hostfxr_initialize_for_runtime_config is artificial. Removing it is actually quite a bit of new work if we were to support comparable behavior to FDD. Currently this entry point "promises" to not load any application code into the default ALC (to avoid version collisions with potential future loaded components). If we were to maintain that promise we would have to implement the split between app/framework assemblies for SCD (per our other discussion - the runtimepack value) and split loading that way. On top of that we would have to support then loading what looks like an SCD component into a secondary load context - currently AssemblyDependencyResolver assumes that files next to the component being loaded (and mentioned in its .deps.json) are part of the component - but for SCD this would be true for the entire framework. So we would have also implement the support for recognizing framework assemblies in AssemblyDependencyResolver (this would be a good thing but still more work). I must also admit I'm a bit hesitant to go this way yet, as it would effectively mean splitting SCD into two halves and there might be small behavior differences which show up. Since this is not a primary scenario for us in .NET 5 (we don't have resources to put into this right now) I want to avoid opening too many new scenarios as there's always a bug tail to be fixed. I'm not against doing this in the future though.

Alternative would be to say that when used on SCD it populates the default ALC with the entire app. But then the usage of it would be quite different. Using the existing entry points which all load into secondary ALCs would be problematic due to potential duplication of assemblies (as mentioned in the doc already). And it would be rather confusing - calling the same function would rather significantly change behavior based on what is the input (not the parameters to the function). This would potentially open weird scenarios in COM/WinRT/IJW as all these entry points call the hostfxr_initialize_for_runtime_config and if given SCD-like input would get the new behavior and at the very least get confused.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honest question - what makes a classlib a classlib? Because when I do dotnet new classlib with .NET 5 Preview 4 it creates a very simple .csproj that seems to be indistinguishable from an app:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>

I'm not trying to argue for hostfxr_initialize_for_runtime_config to support SCD today, I'm just trying to make sure that everyone realizes all of the additional scenarios that this design is opening up. And if the answer is to block all of these additional scenarios, then this is what I was trying to say when I thought going in this direction was a bad idea. If a significant part of the work in implementing this design is adding more blocks, then I don't think it's a good idea to add this "get delegate" functionality inside of hostfxr_get_runtime_delegate.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app project default looks like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

</Project>

The difference is <OutputType>Exe</OutputType>. I do agree that the naming is rather unfortunate (especially in light of Linux/Mac), but it's been like this for many many years so it's not worth changing (at least that's what I've heard about it, I'm not directly involved in this).

This setting drives several behavior differences in the SDK, one of them is that this produces .runtimeconfig.json, which you don't by default for classlib (although we now have properties to enable that).

Logically (at least for me) the functionality belongs into hostfxr_get_runtime_delegate. In ideal world we would enable most of the cases described above and those we mentioned before. In reality we try to prioritize the important scenarios and explicitly block the rest (the reason for that is basically resourcing - the cost is not just adding it now, but maintaining it for a long time - especially since hostfxr is semi-global and thus has a much higher backward compat bar than even the runtime itself in some ways). I don't think that exposing the functionality in a new function helps that much - it might be that the API will be slightly easier to understand (since we can give it scenario specific name and parameters), but it would still have the same limitations. And if we wanted to widen the supported cases in the next release having more APIs tends to make that harder.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested my host with projects that only have <EnableDynamicLoading>true</EnableDynamicLoading> targeting netcoreapp3.x with FDD and SCD without any problems.

If this is going to be in hostfxr_get_runtime_delegate, then I'm interested in the real reasons behind the blocks. And I expect all the people that run into the blocks will be interested as well.

#3660 is already slated in .NET 5.0 and seems to be created explicitly for most of the issues you brought up.

As noted above, SDK support seems to be there already. I never tried to set EnableDynamicLoading through VS but if that's missing then that would affect FDD, too.

I thought hostfxr is already preventing the loading of multiple runtimes into the process.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want my objections to hold this up. Please update the document with the scenarios that should be blocked along with the error message that should be logged so that this can be approved and I can start implementing this.

@vitek-karas
Copy link
Member Author

I added a high-level "vision" for the hosting APIs - this is a result of internal discussion of what we think the hosting APIs should enable in the future.

I also modified the description of the new functionality to make it clearer what combinations are supported.

…tion context

Removes the limitation of `hostfxr_get_runtime_delegate` to make it work on app host context as well.

Defines a new runtime delegate which operates on default load context only.

Some cleanup of the document.
@vitek-karas vitek-karas force-pushed the GetDelegateOnAppProposal branch from 8067de6 to 5fe6cf6 Compare June 3, 2020 18:27
Eventually we could also add functionality for example to setup the isolated load context for unloadability and so on.

Note that there are no APIs proposed in this document which would only perform this step for now. All the APIs so far fold this step into the next one and perform both loading and executing of the managed code in one step. We should try to make this step more explicit in the future to allow for greater flexibility.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to call out that modifying the context (e.g. runtime properties) is no longer possible after this step? (Since we point out it is possible after the previous one)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically it depends on specific APIs, but in general you're right - I will add a comment about that.


Because the helper operates on default load context only it should mostly be used with context initialized via `hostfxr_initialize_for_dotnet_command_line` as in that case the default load context will have the application code available in it. Contexts initialized via `hostfxr_initialize_for_runtime_config` have only the framework assemblies available in the default load context. The `type_name` must resolve within the default load context, so in the case where only framework assemblies are loaded into the default load context it would have to come from one of the framework assemblies only.

It is allowed to call the helper on the first host context (by specifying `NULL` for the context). If the origin of the first context is unknown, it can be the "component" one which only has framework assemblies in the default load context. Same limitation applies as noted above.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NULL for the context -> NULL for the host context handle?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this should have been removed - allowing NULL for context on anything but inspection APIs should be a separate change.


The helper will always load the assembly into an isolated load context. This is the case regardless if the requested assembly is also available in the default load context or not.

**[.NET 5 and above]** It is allowed to call this helper on a host context which came from `hostfxr_initialize_for_dotnet_command_line` which will make all the application assemblies available in the default load context. In such case it is recommended to only use this helper for loading plugins external to the application. Using the helper to load assembly from the application itself will lead to duplicate copies of assemblies and duplicate types.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we no longer want to also relax this to allow NULL for the host context to be the first host context?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it to make the change simpler.
I'm fine adding that functionality as well - but I think it should be a separate change (both to the doc and to the implementation).

vitek-karas and others added 2 commits June 3, 2020 11:50
Co-authored-by: Elinor Fung <47805090+elinor-fung@users.noreply.github.com>
## Longer term vision
This section describes how we think the hosting APIs should evolve. It is expected that this won't happen in any single release, but each change should fit into this picture and hopefully move us closer to this goal. It is also expected that existing APIs won't change (no breaking changes) and thus it can happen that some of the APIs don't fit nicely into the picture.

The hosting layers and APIs should provide functionality to cover a wide range of applications. At the same time it needs to stay in sync with what .NET Core SDK can produce so that the end-to-end experience is available.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This document seems to use hosting components, hosting layers, Hosting, and hostfxr interchangeably but only hosting components is defined in the Terminology section.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to clean this up. hosting components, hosting layers and for the most cases hosting was all changed to hosting components. hostfxr means one of the hosting components, so I left that as is.


Note that there are no APIs proposed in this document which would only perform this step for now. All the APIs so far fold this step into the next one and perform both loading and executing of the managed code in one step. We should try to make this step more explicit in the future to allow for greater flexibility.

After this step it is no longer possible to modify runtime properties (inspection is still allowed) since the runtime has been loaded.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't actually mentioned in this section that the runtime was loaded. I'm not sure it actually belongs in this section, but it's probably too short to create a 5th bucket.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - I added that it needs to load the runtime.

After this step it is no longer possible to modify runtime properties (inspection is still allowed) since the runtime has been loaded.

### Execute managed code
The end result of this step is either execution of the desired managed code, or returning a native callable representation of the managed code.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that returning a native callable representation of the managed code is actually an implementation of the Loading managed code bucket and that the execute step is then performed by the native host on the function pointer that was created.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the title of this section - the intent was to separate "loading the code" from "accessing/running the code". For example one reason to do this is that typically the caller will want to load the code just once, but may want to access/run it several times.

* loading application into an isolated load context - for now there are too many limitations in the frameworks around secondary load contexts (several large framework features don't work well in a secondary ALC), so the hosting should prevent this as way to prevent users from getting into a bad situation.
* loading multiple copies of the runtime into the process - support for side-by-side loading of different .NET Core runtime in a single process is not something we want to implement (at least yet). Hosting should not allow this to make the user experience predictable. This means that full support for loading self-contained components at all times is not supported.

Lot of other combinations will not be allowed simply as a result of shipping decisions. There's only so much functionality we can fit into any given release and so many combinations will be explicitly disabled to reduce the work necessary to actually ship this. The document below should describe which combinations are allowed in which release.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given release and so many -> given release, so many

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

### Combinations
Ideally it should be possible to combine the different types of initialization, loading and executing of managed code in any way the native host desires. In reality not all combinations are possible or practical. Few examples of combinations which we should probably NOT support (note that this may change in the future):
* running a component as an application - runtime currently makes too many assumptions on what it means to "Run an application"
* loading application into an isolated load context - for now there are too many limitations in the frameworks around secondary load contexts (several large framework features don't work well in a secondary ALC), so the hosting should prevent this as way to prevent users from getting into a bad situation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as way to prevent -> as a way to prevent

@@ -316,7 +384,7 @@ Closes a host context.
* `host_context_handle` - handle to the initialized host context to close.


### Loading managed components
### Loading and calling managed components
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description of hdt_load_assembly_and_get_function_pointer above was referring to this section's name and should be updated as well.

* `delegate` - out parameter which receives the native function pointer to the requested managed method.

It is allowed to call the returned runtime helper many times for different assemblies or different methods from the same assembly. It is not required to get the helper every time. The implementation of the helper will cache loaded assemblies, so requests to load the same assembly twice will load it only once and reuse it from that point onward. Ideally components should not take a dependency on this behavior, which means components should not have global state. Global state in components is typically just cause for problems. For example it may create ordering issues or unintended side effects and so on.
The helper will lookup the `type_name` from the default load context (`AssemblyLoadContext.Default`) and then return method on it. If the type lookup requires any assemblies to be loaded, they will be resolved and loaded in the default load context.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above it says that this is for already available managed method, so why are we talking about resolving dependencies? Is the implementation going to use an AssemblyDependencyResolver or rely only on the default ALC's normal resolution behavior?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation should not use AssemblyDependencyResolver as it doesn't have the "component/assembly" to use it on. By the above description I was trying to say that the act of looking up an assembly qualified type name may cause assembly resolution to occur (if the assembly is not yet loaded into the default ALC). Runtime is VERY lazy in loading assemblies and only does so at the last moment it has to. So when the app starts the default ALC will have loaded only the necessary assemblies to get to Main, nothing else. If the requested type comes from assemblies outside of that set, the default ALC will perform normal assembly resolution step - in this case almost certainly resolved from TPA and loaded. Getting a method from the type may itself trigger additional assembly resolution/loading (for example if the method has a parameter type from a different assembly) and so on.

I'll try to make this clearer.


It is allowed to ask for this helper on any valid host context. Because the helper operates on default load context only it should mostly be used with context initialized via `hostfxr_initialize_for_dotnet_command_line` as in that case the default load context will have the application code available in it. Contexts initialized via `hostfxr_initialize_for_runtime_config` have only the framework assemblies available in the default load context. The `type_name` must resolve within the default load context, so in the case where only framework assemblies are loaded into the default load context it would have to come from one of the framework assemblies only.

It is allowed to call the returned runtime helper many times for different types or methods. It is not required to get the helper every time.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statement below should not mention managed component, everything is loaded into the default context in this current design.

@@ -300,12 +368,12 @@ Starts the runtime and returns a function pointer to specified functionality of
* `hdt_load_assembly_and_get_function_pointer` - entry point which loads an assembly (with dependencies) and returns function pointer for a specified static method. See below for details (Loading managed components)
* `hdt_com_activation`, `hdt_com_register`, `hdt_com_unregister` - COM activation entry-points - see [COM activation](https://github.com/dotnet/runtime/tree/master/docs/design/features/COM-activation.md) for more details.
* `hdt_load_in_memory_assembly` - IJW entry-point - see [IJW activation](https://github.com/dotnet/runtime/tree/master/docs/design/features/IJW-activation.md) for more details.
* `hdt_winrt_activation` - removed WinRT activation entry-point. This delegate is not supported for .NET 5 or newer.
* `hdt_winrt_activation` **[.NET 3.\* only]** - WinRT activation entry-point - see [WinRT activation](https://github.com/dotnet/runtime/tree/master/docs/design/features/WinRT-activation.md) for more details. The delegate is not supported for .NET 5 and above.
* `hdt_get_function_pointer` **[.NET 5 and above]** - entry-point which finds a managed method and returns a function pointer to it. See below for details.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably mention the section name like hdt_load_assembly_and_get_function_pointer does.

@vitek-karas vitek-karas merged commit 70cbb23 into dotnet:master Jun 5, 2020
@vitek-karas vitek-karas deleted the GetDelegateOnAppProposal branch June 5, 2020 07:13
@ghost ghost locked as resolved and limited conversation to collaborators Dec 9, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
0