-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
C++/CLI assembly is loaded into isolated AssemblyLoadContext (IsolatedComponentLoadContext) in .NET 6 #61105
Comments
Tagging subscribers to this area: @vitek-karas, @agocke, @VSadov Issue DetailsThis is a continuation of the discussion in #56391 which is closed and therefore cannot be commented on. @jkoritzinsky said:
Can we expect this limitation to be removed? This is a major migration blocker for our (native/unmanaged) C++ application that used C++/CLI to implement some of its functionality. C++/CLI dlls are called via the PE import table and LoadLibrary/GetProcAddress and it will be very, very difficult to track down & change all these code paths. Is there a way to make this customizable via some event, or runtime config? I already "host" the runtime as described here.
|
One of the workarounds described is pre-loading via an "initialize" stub. Can you elaborate on why that wouldn't work for you? Your description here:
doesn't sound like it should be necessary, since you shouldn't need to change the calls, as long as the assembly is first loaded via a managed shim. |
The workaround works. I implemented in the first scenario that we hit. The problem is that the application is very large. C++/CLI dlls get loaded in many code paths. The same C++/CLI dll may be loaded via one code path or another depending on what the user does. Loading all C++/CLI dlls on startup is non-starter because of the performance cost. Essentially, I'm trying to highlight the significant migration cost in hopes that I can change the design decision that caused IsolatedComponentLoadContext to be applied (or at least give the host control over this). |
The decision to use an
One possible way to work around this would be to fork the ijwhost and have it call into your own method in the runtime to forcibly load it and all of its dependencies into the default ALC. |
@jkoritzinsky Thanks for the details.
I'm probably demonstrating huge amount of ignorance here. :-) Thank you in advance for accommodating me. |
When loaded from managed code, there are two cases. One, the C++/CLI assembly and its dependencies are already in the dependency list of the assembly that loaded it, so it can be loaded in the same ALC as the calling assembly, which in most cases is the default ALC. Otherwise, the developer has used the ALC or Assembly APIs to explicitly load a C++/CLI assembly in a particular ALC, so the runtime tries to load it as best it can.
That's because |
So why couldn't the default ALC be used when the caller has no ALC (because it is unmanaged code)? It seems rather arbitrary to create a new IsolatedComponentLoadContext in this case. In other words, I still don't see the advantage of creating an isolated context. I see the disadvantage: it breaks .net4 apps that are trying to move to net6 platform. |
What if someone tries to run another C++/CLI assembly as a plugin that should be fully isolated? If we don't create a separate ALC for each component, than every component after the first could end up with non-deterministic failures since some assemblies (and as a result values of static fields) might be shared while others aren't and the set of shared assemblies would depend on which component was loaded first. For example, let's take the following setup:
Since calls from A would fall into your "no ALC" case, that would mean that calls to B or D from A would both load into the default ALC. In this scenario, if B is called first, then assembly C version 1 would be loaded into the default ALC. Then, if A tries to call the COM component D, it would try to load assembly C version 2 into the default ALC and would fail since C version 1 is already loaded. As a result, there would be no safe mechanism to load two components at the same time as any dependency might conflict. By isolating B and D and their dependencies each into their own ALCs, they can each load their required version of C and A can successfully call into both. We can't block loading multiple components because that effectively makes the component story useless. |
You're expected to provide a .deps.json as part of The alternate way is to manually add your assemblies to the TPA through a runtime property (I don't remember what it is, maybe |
They can always do this by creating their own ALC. My point is that .net4 didn't have ALCs. Assemblies got loaded into the "default" context all the time (yes, sometime causing failure). I think the path of least surprise for migration is to maintain this behavior. Clients who wish to have more advanced plugin/component story can start using ALC. |
Assembly loading is one of the areas where .NET Core/.NET was intentionally made different. Just like .NET Core/.NET doesn't have app domains. The magic of fusion in .NET Framework worked reasonably well for simple cases, but more complex cases caused all kinds of trouble (probably the most known is binding redirect hell). .NET Core was intentionally designed to have a much simpler assembly loader, where most things are specified explicitly. This can be either by the SDK (.deps.json) or by the code (ALCs, and the various loader extension points). Yes, it causes some friction when you try to migrate a large system from .NET Framework, but it also creates lot of clarity and robustness (way less surprises). The case you mention where components were just loaded automatically onto one big pile frequently caused problems and the need for complex binding redirects to keep the system working. |
@vitek-karas Fair enough. Thanks for the details. A data point: the unmanaged host that I work on (AutoCAD) has hosted .NET Framework since 2004 with many C++/CLI and C# assemblies. The "big pile" has not caused any trouble (no need for binding redirects). Perhaps, we have been lucky. :-) I'll pursue @jkoritzinsky's suggestion to see if a forked/custom built ijwhost.dll could help ease our migration pain. Thanks for the discussion. I hope this will be useful for others. |
I'm not saying that it should not be possible to load everything into default, just that having that as the default is problematic. Unfortunately currently we don't have a setting to allow this easily. |
Working around this by modifying ijwhost.dll is not super pretty. The changes in ijwhost.dll are simple but unfortunately AssemblyLoadContext.LoadFromInMemoryModule is
|
I would like to piggyback on this issue and note that it has been a significant roadblock to the migration of my company's 250+ project solution. We have several C++/CLI projects and it isn't reasonable to expect us to invoke a workaround "Initialize()" method in the C++ projects for every single C# assembly. At least, that is how I'm interpreting this workaround for the moment. |
The problems you experience due to this issue will manifest when native C++ code calls C++/CLI code (or C# code calls C++/CLI code via DllImport but this looks weird so I expect it to be very infrequent). If you experience an issue when calling from C# to C++/CLI via a managed entry point then it must be different problem. |
The path is: C# -> Create object defined in C++/CLI -> the C++/CLI object then invokes a method in another C++/CLI project -> The second CLI library calls back into C# and that's when I pick up that the C# library has a context IsolatedComponentLoadContext. |
I suspect that this call is via a native export so this is where things go south. |
That appears to be the case. The class that makes the call has mixed code - some methods are native, some are managed. It's extremely funky for lack of a better word. The call to initialize the singleton instance in the C# code passes from a native method to a managed method in the same class. Adding an Initialize() method and invoking it from C# before heading into the CLR code seemed to fix it, though I'm really not a fan of having to do this to work around the problem. |
I have put together a repro case of this which demonstrates issue concisely with the "dummy/hack" method workaround: https://github.com/fiercekittenz/CLIAssemblyLoadContextDemo |
Thanks all for sharing your feedback/pain around this. Based on all the feedback we have received, we will be loading C++/CLI DLLs built targeting .NET 7.0 into the default Getting this behaviour will require rebuilding the C++/CLI component (to pick up the updated |
Closing based on #66486 |
This is a continuation of the discussion in #56391 which is closed and therefore cannot be commented on.
@jkoritzinsky said:
Can we expect this limitation to be removed? This is a major migration blocker for our (native/unmanaged) C++ application that used C++/CLI to implement some of its functionality. C++/CLI dlls are called via the PE import table and LoadLibrary/GetProcAddress and it will be very, very difficult to track down & change all these code paths.
Is there a way to make this customizable via some event, or runtime config? I already "host" the runtime as described here.
The text was updated successfully, but these errors were encountered: