mcpiroman / UnityNativeTool

Allows to unload native plugins in Unity3d editor
MIT License
183 stars 18 forks source link

Can't figure out why the native dll isn't unloading #6

Closed chrisk414 closed 4 years ago

chrisk414 commented 4 years ago

Hi, I've been pulling lots of hairs trying to figure out how to unload a native dll and I came across to this wonderful solution.

I tried to set it up and make sure I follow the direction but it doesn't seem to work. It runs fine for the first time but Unity Editor freezes at the second run. Assets.zip

I'm including a sample project for the testing. I'll really appreciate if you can help why it's not working. Cheers

mcpiroman commented 4 years ago

In your RealsTodoTest.cs file you seem to try to unload DLL on your own (UnloadModule()). Are you sure this doesn't mess up things? (As a side note, if you're willing to use say kernel32 with this tool, make sure to utilize [DisableMocking] lest it tries to load that DLL manually too).

Secondly, your sample script doesn't make any use of provided DLL. Does the Unity freeze/crash even in that situation (I mean, if you don't even call your DLL)?

If the Editor crashes, it may be helpful if you send me its logs. On Windows, you should find them in C:\Users\USER\AppData\Local\Temp\Unity\Editor\Crashes or C:\users\USER\AppData\Local\Unity\Editor, preferably all of them (make sure to cut from them any sensitive information if there is any).

The DllManiupulatorScript should show some information about loaded DLLs. Does it match your expectations (up to a point of crash ofc)? Also check the console log for warnings about DLL being not found or sth like thhis.

chrisk414 commented 4 years ago

Hi, thank you very much for your help. I provided the example so that you can run it yourself. Have you had a chance to try it?

Anyway, to answer some of your questions,

  1. UnloadModules is not really needed as long as it gets unloaded. You can just comment it out. I was just trying everything I can trying to unload.

  2. I'm not sure what [DisableMocking] does. Please let me know if how I should try.

  3. The native dll, in my case, is realm-wrappers.dll. And it is called from Realm.dll. They both are grabbed from NuGet. My sample code calls Realm APIs in Realm.dll, and in turn, it load realm-wrappers.dll that cause freeze while trying to unload at the next AppDomain reload.

  4. My application doesn't freeze unless I call Realm API. If you comment out the line, for example, AddEntry(realm, "Todo Entry: 1 "); it won't freeze. AddEntry will p/invoke a call inside of realm-wrappers.dll

  5. Yeah, DllManipulatorScript shows that realm-wrappers.dll is loaded property but it will freezes during AppDomain reload.

I hope my case helps to improve this wonderful utility. I really hope we can solve the freezing problem. Please let me know if I need to explain more. I really appreciate for your help.

Cheers!

chrisk414 commented 4 years ago

FYI, you can take a look at Realm.dll from here. https://github.com/realm/realm-dotnet

All I did was to remove "UnmanagedType.LPArray" and compiled it.

Realm.Tests.dll just contains Item RealmObject. I had to compile outside of Unity because it needs to be Weaved.


    public class Item : RealmObject

    {
        [PrimaryKey]
        [MapTo("itemId")]
        public string ItemId { get; set; } = Guid.NewGuid().ToString();

        [MapTo("body")]
        public string Body { get; set; }

        [MapTo("isDone")]
        public bool IsDone { get; set; }

        [MapTo("timestamp")]
        public DateTimeOffset Timestamp { get; set; }
    }
mcpiroman commented 4 years ago

Ah I see, native DLL is called through the managed DLL, not directly via script. This complicates things.

In options, there is one called 'Only executing assembly' and is enabled by default. This means that this tools works only in the scope of, in most cases, Assembly-CSharp assembly and not in your wrapper DLLs. If you uncheck it,you may choose additional assemblies, but apparently the DLLs in Plugins folder. Quite unfortune, I'll look at that.

Therefore, I'm fairly surprised that DllManipulatorScript showed you that realm-wrappers.dll is loaded. I've run your example and it didn't even list it (as I expected).

So you may walk around the thirst problem by changing logic in code to simply mock all loaded assemblies (that's what I did), but there is second problem. This tool mocks all [DllImport]s it founds unless they have [DissableMocking] attribute (check documentation), so including kernel32.dll and so on. This means that you would either have to add [DissableMocking] to such imports, or have files like Assets/Plugins/__kernel32.dll in order to make it work.

This means that this tool doesn't support your scenario right now, but neither should it crash (or even behave as you described). Since I cannot reproduce your situation, I'll need some more data (like the logs). And if it matters, I cant make it to the AddEntry(realm, "Todo Entry: 1 "); line because obviously I don't have a server that would login a user.

chrisk414 commented 4 years ago

Hi, Yeah, unable to choose my own managed assembly was one problem I was going to ask you about after I resolve the freezing issue. But just for the test, I hardcoded the assembly. Please take a look at DllManipulatorScript I provided. You will see the modifications.


         // added -chrisk
            Options.assemblyPaths = new[]
            {
                Application.dataPath + "/Plugins/Realm.dll",
                Application.dataPath + "/Plugins/Realm.Tests.dll",
            };

Oh, the server login is not needed. Please just comment out the following line. user = await Login("realm-admin", "", serverAddress);

Please let me know if you have a problem running the sample. And please let me know what I should try next if there are. ^^

Cheers!

mcpiroman commented 4 years ago

Ok then, I was using my, unmodified version. I'll test it once I have an opportunity.

mcpiroman commented 4 years ago

I can indeed reproduce the editor freeze at second run. It happens before any of C# code gets run, so it might be some Unity error/thing. What's more, DLL doesn't get unloaded even though FreeLibrary() succeeds.

I've made similar sample project with wrapper .Net Standard DLL and everything worked OK.

HOWEVER, I've just run this on plain Unity without this tool (and renamed __realm-wrappers.dll back to realm-wrappers.dll) and Unity editor froze as well. Does this happen to you? I strongly feel that this is not my issue.

chrisk414 commented 4 years ago

Yeah, FreeLibrary doesn't really free anything. It returns success regardless.

Unity freezes because the native dll cannot be unloaded. I believe some native calls are not wrapped correctly.

"Tool created mainly to solve old problem with reloading native plugins without the need to reopen Unity Editor."

That's why I'm using your tool, hoping that it will help fix the problem.

I'm still hoping... ^^

Cheers!

mcpiroman commented 4 years ago

The concerned problem is that Unity doesn't unload native plugins on its own (and can't be kindly asked to).

In this case however there is a bug in the plugin itself (or maybe in Unity). And since Windows can't unload it, neither can I. You may force it to unload via something like Process Hacker, but it doesn't prevent editor from hanging.

I think you'd be better off finding the problem with the plugin. Perhaps there is some detached thread that prevents it from unloading? Or deadlock?

Btw. When you run your sample (with plain Unity) the editor sometimes can't be exited even though it doesn't freeze.

chrisk414 commented 4 years ago

I tried to check to see if there is a thread still running inside of native dll, but I don't think there is any thread that prevents the unloading.

Here is something I don't understand.

When a native dll called with P/Invoke, Unity cannot unload the dll, thus it causes the freeze. Your utility is trying to fix this problem by replaces P/Invoke with dynamic calls so that the dll can be unloaded, correct? Otherwise, I don't quite understand what your utility is trying to do. Perhaps you missed replacing something that still calls the dll with P/Invoke?

Thanks for the help.

mcpiroman commented 4 years ago

Unfortunately you're rather wrong: When Unity (Mono to be specyfic, and any other .Net application) executes P/Invoke function, it loads the specified DLL, unless it is already loaded (so only on the first call to it). It then keeps it loaded for the lifetime of application, where in case of Unity editor it's the lifetime of the editor itself, and not, say, when you pause the game, because this is still the same process. Unity simply can't do much about it because this is all handled by Mono, which doesn't expose any hooks that would allow for an ealier unload (afaik). There is no opportunity to freeze whatsoever, the plugin is just kept loaded.

Usually this behaviour is fine, unless when you're developing the plugin. Because it is kept loaded you can't just recompile it and replace the DLL file with new one. You have to close editor, then replace the file and then load the project again. This is why I made this - to allow unloading and loading plugins at any time, so they can be replaced. Usually the most convenient moment is when you stop the game, but it could be e.g. when you just pause it.

Behaviour you're facing is definitely not expected. You should file this issue to the plugin's origin repo, if they state that Unity or Mono is supported. You may also want to check if your sample code does run on plain Mono and plain .net framework (and I wouldn't be much surprised if it's Mono's fault, I've found 4 or 5 bugs in it while working on this project).

chrisk414 commented 4 years ago

Hmm... I don't understand.

  1. Native utility with p/invoke cannot be unloaded and your utility cannot help here.
  2. Native utility without p/invoke will by unloaded just fine by AppDomain reload. Your utility is not really needed to unload.

What's the point of this utility?

I thought your utility turns p/invoke calls to dynamic calls so that it can be unloaded.

mcpiroman commented 4 years ago

2. Native utility without p/invoke will by unloaded just fine by AppDomain reload. Your utility is not really needed to unload.

Unfortunately AppDomain.Unload(AppDomain.CurrentDomain) doesn't work in Unity ( and sometimes event freezes editor). It might be yet another bug in Mono, nonetheless it won't help. Maybe this could be achieved in some other way that I'm not aware of?

I thought your utility turns p/invoke calls to dynamic calls so that it can be unloaded.

Excellently. It prevents Mono from loading the library and instead handles it itself, with the difference that it also unloads it later.

  1. Native utility with p/invoke cannot be unloaded and your utility cannot help here.

This is because this native utility is faulty. If FreeLibrary doesn't work, I won't help here. My aim is to hack Unity, not Win32.

chrisk414 commented 4 years ago

Hmm.. again, if FreeLibary can unload the native library, I didn't need to use your utility at all. It's that FreeLibrary cannot unload native dll with p/invoke. I expected your wrapper to remove p/invoke calls so that it can be unloaded by FreeLibrary.

mcpiroman commented 4 years ago

I'm posting this one here in case someone else finds that useful.

You're correct that this tool is not essential to unload DLLs that Unity/Mono loaded for the sake of PInvoke. On Winodws, you may use something like:

ProcessModule dll = ...; //Find interesting dll in loaded modules
FreeLibrary(dll.BaseAddress);

or even

var handle = LoadLibrary("dll_name");
FreeLibrary(handle);
FreeLibrary(handle);

However, this won't be of much use. This way you've just stolen the Dll from under the runtime's nose, but it's unaware of this fact. Next time you do PInvoke you'll just crash.

So you may load the DLL again, before any PInvoke, but it may load at different address. Even if not, the symbols may be at different addresses, especially if you modified the DLL in the interim. This will all lead to crash, at best.

Secondly, you may opt out of the 'automatic' PInvoke and resolve it manually. On Windows, this would look like this:

var dllHandle = LoadLibrary("sth");

// For each function
delegate void Func1Del(int arg);
Func1Del func1;
var func1addr = GetProcAddress(dllHandle, "func1");
func1 = Marshal.GetDelegateForFunctionPtr(func1addr);
func1(123);

//Some time latter
FreeLibrary(dllHandle);

But this approach has many disadvantages:

I've seen people doing so though.

My solution aims to provide the best of both worlds: you may still use [DllImport] syntax and, in production builds, it's behaviour, and at development time you may also reload DLLs in convenient way. That's the intended purpose at least.

Although I've sought for better and simpler ways to achieve this, I may have missed something. If you know any better solution, let me know.

It's that FreeLibrary cannot unload native dll with p/invoke.

It can't unload your particular dll. In general it can, but I've discussed it above.

I expected your wrapper to remove p/invoke calls so that it can be unloaded by FreeLibrary.

It really does so, although that statement is not 100% correct. It removes the behaviour of [DllImport] and replaces it with manual loading of DLLs and functions, similarly to the snippet I wrote above, but that's still PInvoke, just more 'manual' one.

mcpiroman commented 4 years ago

As to your problem, this tool probably won't help you. The problem lives somewhere in the Realm library, Mono or Unity.

If you look at https://realm.io/docs/dotnet/latest/#prerequisites it lists .Net Framework and .Net Core as supported, but not Mono Framework. On the other hand it also lists Xamarian which afaik is based on Mono, so idk. I would just ask for help at the library's repo.

I'm closing this issue as external to the project. If I can somehow help you, feel free to ask.