Mono: Trouble with reloading assemblies/images and domain

Viewed 229

I have a question that I can't handle for a week, you won't believe it, I've already checked everything that is possible, but it still doesn't want to work correctly. Now I'm writing a script system for my game engine and I need to dynamically recomile/reload .the dll file that contains my library and scripts. To do this, I do:

  1. Initialize Mono. (I get an appdomain (MonoDomain*) already created for me)
  2. Open an assembly file. (dll)
  3. Get an image from this assembly.
  4. Use mono (I load internal calls, create C# objects, call functions, etc.).

But at some point I need to recompile/reload this assembly, because I change the script code, well, it's understandable. As I understand from a lot of read documentation and various sources, I do not have to describe the domain, assemblies and images while it is active and for this you first need:

  1. Create a new domain.
  2. Upload the image.
  3. Connect the image to the object assembly (thereby making the image status OK)
  4. And only then describe the resources from the last domain.

But just so that I don't try, either an error occurs in the mono code, or now I have reached the stage that there are no errors, but here is a pointer to a new image obtained from a newly loaded assembly (with the same name) is the same and thus it turns out that mono continues to use the same image and does not have the updated script values.

This is code of my Mono init:

    mono_set_dirs(".", ".");

    // 0. Load first domain, init mono + appdomain + load assembly.
    const char* domainName   = "ForceCS";
    const char* assemblyName = "ForceCS.dll";
    pMonoDomain = mono_jit_init(domainName);
    if (pMonoDomain) {
        pMonoAssembly = mono_domain_assembly_open(pMonoDomain, assemblyName);
        if (pMonoAssembly) {
            pMonoImage = mono_assembly_get_image(pMonoAssembly);
            if (pMonoImage) {
                AddInternalFunctionsAndScriptClasses(); // mono_add_internal_call ...
            }
        }
    }

Then i do some stuff with my dll like create objects or call methods as i said before. And then try to reload the ForceCS.dll.

    // 1. Load new domain (Works fine).
    char* newDomainName = "ForceCS";
    MonoDomain* newDomain = mono_domain_create_appdomain(newDomainName, NULL);
    if (!mono_domain_set(newDomain, false)) {
         // Cannot set domain!
    }

    // 2. Load image. (Works fine)
    const char* dllFile = "C:\\Force\\Editor\\ForceCS.dll";
    MonoImageOpenStatus status;
    MonoImage* newImage = mono_image_open(dllFile, &status); ** Problem here: Shows the same address as before and that means that Mono use same .dll.** 

    // 3. Load new assembly. (Works fine).
    MonoAssembly* assembly = mono_assembly_load_from(newImage, mono_image_get_name(newImage), &status);
    if (status != MonoImageOpenStatus::MONO_IMAGE_OK) {
        ///Cannot load assembly!
    }

    assembly = mono_assembly_open(dllFile, &status);

    newImage = mono_assembly_get_image(assembly);
    if (newImage) {
        AddInternalFunctionsAndScriptClasses();
    }

    // 4. Inload old domain.
    MonoDomain* domainToUnload = pMonoDomain == nullptr ? mono_domain_get() : pMonoDomain;
    if (domainToUnload && domainToUnload != mono_get_root_domain()) {
        mono_domain_unload(domainToUnload);

        mono_gc_collect(mono_gc_max_generation());
        mono_domain_finalize(domainToUnload, 2000);
        mono_gc_collect(mono_gc_max_generation());

        pMonoDomain = newDomain;
        pMonoImage = newImage;
        pMonoAssembly = assembly;
    }

Final problem that image always stays the same, only it works if i load this assembly with different name like ForceCS1.dll, but its not what i want. Please explain me:

  1. When i need to close/free domains, assemblies, images.
  2. What the connection between assembly-image.
  3. How reload my .dll assembly.

I will be grateful for every answer.

1 Answers

Finally, after another week of torment, I finally managed to fix it. In general, as I understood the problem was that I was unloading my only main domain, but I did not create/set to another domain for mono so that he could access it on another thread while the assembly is being reloaded.

In general, I completely rewrote the code, I got something like:


int main() {

    // Init Mono Runtime. (Create Root Domain)
    mono_init();
    if (mono_create_domain_and_open_assembly("Scripting Domain", "MyScript.dll"))
        mono_invoke("Script", "OnCreate", 0, NULL, NULL);

    while (true) {
        bool is_reload_request;
        std::cin >> is_reload_request;

        if (is_reload_request) {
            mono_destroy_domain_with_assisisated_assembly(domain);

            // Update new compiled assembly.
            mono_update_assembly("C:\\MonoProject\\MyScript.dll", "C:\\MonoProjectC#\\Binary\\Debug-windows-x86_64\\MyScript.dll");

            if (mono_create_domain_and_open_assembly("Scripting Domain", "MyScript.dll"))
                mono_invoke("Script", "OnCreate", 0, NULL, NULL);
        }
    }

    mono_jit_cleanup(root_domain);
    return 0;
}

Now I want to explain in detail what I did, for those who may also have encountered such a problem and cannot understand what and how and how to fix it.

Init Mono & create root domain.

The first thing I did was initialize mono runtime as before, thereby creating a root domain so that in case of a reloading we could switch to it.

void mono_init() {
    mono_set_dirs(".", ".");
    root_domain = mono_jit_init_version("Root Domain", "v4.0.30319");
}

Creating Scripting domain, loading assembly.

Then I created a new domain in our case, this is the Scripting Domain, and after creating it, I set it for mono (mono_domain_set(domain, 0)) so that all subsequent actions are performed from it. And then, just like before, I opened the my assembly MyScript.dll through the domain and got pointers to the assembly and the image.

bool mono_create_domain_and_open_assembly(char* domain_name, const char* assembly_file) {
    domain = mono_domain_create_appdomain(domain_name, NULL);
    if (domain) {
        mono_domain_set(domain, 0);

        // Open assembly.
        assembly = mono_domain_assembly_open(mono_domain_get(), assembly_file);
        if (assembly) {
            image = mono_assembly_get_image(assembly);
            if (image)
                return true;
        }
    }
    return false;
}

After this stage, I checked that my assembly was working fine, just created a Script object and called the OnCreate method from it. I will not write this code so I hope those people who have already worked with mono know how to do it.

Reloading

Now the reloading itself. In fact, everything is much simpler here than it seems, just before calling mono_domain_upload, you need to call mono_domain_set to switch our Scripting Domain to Root Domain for mono when we will creating new domain (new dll).

bool mono_destory_domain_with_assisisated_assembly(MonoDomain* domain_to_destroy) {
    if (domain_to_destroy != root_domain) {
        mono_domain_set(root_domain, 0);
        mono_domain_unload(domain_to_destroy);
        return true;
    }
    return false;
}

And then compile our C# project and move it to our project (or where you specified the paths for mono). Or how I did it through C++:

void mono_update_assembly(char* assembly_path, char* assembly_path_from) {
    if (std::filesystem::exists(assembly_path_from)) {
        
        if (std::filesystem::exists(assembly_path))
            std::filesystem::remove(assembly_path);

        std::filesystem::copy(assembly_path_from, assembly_path);
    }
}

And the last step after all that has been done, we just create new domain and load new assembly.

Related