问题
I am kinda stuck on not being able to dispose .NET 3.5 dlls from the process. AppDomain support is off in unity, there is no way to unload a dll from the process using the .NET api, because the C# functions are not implemented.
Anyone could get me some hints on how / where should I start to remove the dll from the memory / process somehow, so I can re-load the dll whenever I want?
回答1:
Alright, after this time I thought that this is some sort of heavy research, and there are no publications related to this. So here is what you have to do. First head over to https://github.com/mono/mono/branches/all and determine which mono version you are going to require. In my case I was doing this for an old game, and I needed the 2014 version of mono.
My project is discontinued so there is no point for me to keep this as a secret. The following examples will show you a way, but It probably won't be enough for you to get what you want on a newer mono version.
Using this way, you are able to unload every dll in a new AppDomain. You can extend It to create multiple appdomains, or unload only a specific dll. If It is working on a newer mono version then please let me know! Credits go to me, and 4g3v.
Once you have done that you are going to need exactly the same environment, and compilers for that version. In my case that was the Visual Studio 2010 compilers, and I didn't need to refactor most of the things.
You will need more than just following my instructions, you will have to play with mono, and get to know how the project works.
So hence as stated: C# API doesn't support AppDomains, but mono by default does. You just need to make some improvements, and extensions for It. Here is what you need to do:
Define two new functions in mono.def for example mono_rb_create_domain and mono_rb_unload_domain
The above two functions will be responsible to create, and dispose a domain.
Head over to: mono/metadata/object.c Find function mono_runtime_unhandled_exception_policy_set and add (We will create the function later below):
mono_add_internal_call("YourDLLNameSpace.Icalls::mono_rb_load_plugin", ves_icall_mono_rb_load_plugin); //Last mono function unity calls before adding their own icalls (mono_runtime_unhandled_exception_policy_set). Adding them at runtime doesn't work, so this should be a pretty good place.
The above code will define a C# function that will be able to handle the loading of a custom DLL loaded in our own AppDomain. Make sure that your C# class, and function is public. Reminder: This should be somewhere in your unity project already. (For example Assembly-CSharp or whatever). It is crucial, because this will be handling the loading of your new dlls, and they will go to a new appdomain.
[MethodImpl(MethodImplOptions.InternalCall)]
public extern Assembly mono_rb_load_plugin(IntPtr data, uint dataLen);
Alright, we have to add some additional checks to avoid crashes in unity/unity_liveness.c Look for function mono_add_process_object and make it look like the following. This might differ in newer mono versions. =) What we have done here is basically ensuring that the received object has a VTable (which should be the class), and It isn't null.
static void mono_add_process_object (MonoObject* object, LivenessState* state)
{
gboolean has_references = 0;
MonoClass* klass; // Define the class
if (object && !IS_MARKED(object))
{
klass = GET_VTABLE(object)->klass; // Get the VTable
// Ensure that the class isn't f***ed up. Read: https://en.wikipedia.org/wiki/Hexspeak
if(klass == NULL || klass == 0xBAADF00D || klass == 0xFEEEFEEE)
{
return;
}
has_references = klass->has_references;
if(has_references || should_process_value(object,state) != LIVENESS_DONT_PROCESS)
{
if (array_is_full(state->all_objects))
array_safe_grow(state, state->all_objects);
array_push_back(state->all_objects, object);
MARK_OBJ(object);
}
// Check if klass has further references - if not skip adding
if (has_references)
{
if(array_is_full(state->process_array))
array_safe_grow(state, state->process_array);
array_push_back(state->process_array, object);
}
}
}
The above code will ensure that the processed class isn't faulty, or points somewhere else where It shouldn't.
Let's create our domain handlers. Make sure to create this under the mono project.
My header file was named as mono_rustbuster.h, and contained:
static MonoDomain* rustBusterDomain;
GHashTable* pluginHashTable;
void mono_method_info_object();
MonoReflectionAssembly* ves_icall_mono_rb_load_plugin(void* objectPtr, char* data, guint32 dataLen) MONO_INTERNAL;
Then we created mono_rustbuster.c, and wrote the following:
#include "metadata\metadata-internals.h"
#include "metadata\image.h"
#include "metadata\assembly.h"
#include "metadata\debug-helpers.h"
#include "metadata\class-internals.h"
#include "metadata\object-internals.h"
static MonoDomain* rustBusterDomain;
GHashTable* pluginHashTable;
void mono_method_info_object()
{
}
int mono_rb_create_domain()
{
pluginHashTable = g_hash_table_new(g_str_hash, g_str_equal);
rustBusterDomain = mono_domain_create_appdomain("PluginDomain", NULL);
return 0x01;
}
int mono_rb_unload_domain()
{
mono_domain_unload(rustBusterDomain);
return 0x01;
}
MonoReflectionAssembly* ves_icall_mono_rb_load_plugin(void* objectPtr, char* data, guint32 dataLen)
{
MonoAssembly* ass;
MonoImage* img;
MonoImageOpenStatus status;
MonoDomain* current;
char *assNameBuf;
current = mono_domain_get();
mono_domain_set(rustBusterDomain, FALSE);
img = mono_image_open_from_data_full(data, dataLen, TRUE, NULL, FALSE);
ass = mono_assembly_load_from_full(img, "", &status, FALSE);
assNameBuf = (char*)malloc(256);
sprintf(assNameBuf, "%s", ass->aname.name);
g_hash_table_insert(pluginHashTable, (gpointer)assNameBuf, (gpointer)ass);
mono_domain_set(current, FALSE);
return mono_assembly_get_object(rustBusterDomain, ass);
}
After this setup your loaded DLLs might whine about missing references. This mostly happens when you load a new DLL in all by yourself using these functions. We added some layers into mono/metadata/assembly.c for this fix. Find mono_assembly_load_reference This method works with the assembly's references, find where It is calling the reference variable and add:
if(reference == NULL && strstr(image->name, "data-"))
{
reference = (MonoAssembly*)g_hash_table_lookup(pluginHashTable, aname.name);
}
Hint: Mono add's data- to all of the dll's, so that way we can use that memory pointer to find the references. Basically fixes the unresolved referencess.
Head over to mono/metadata/class.c and find: mono_class_is_assignable_from
Before function would return data using the last function check if the class names are equal. The last return looks something like this:
return mono_class_has_parent (oklass, klass);
Add:
if(!mono_class_has_parent (oklass, klass))
{
if(strstr(klass->image->name, "data-"))
{
if(!strcmp((oklass)->supertypes [(klass)->idepth - 1]->name, klass->name))
{
//OutputDebugStringA("mono_class_is_assignable_from(): Class names are equal so true is returned");
return TRUE;
}
}
}
The above code will add fixes to ScriptableObject cast exceptions.
You are sort of done. You may encounter additional issues. Here is how it works in C#:
[DllImport("mono.dll")]
internal static extern int mono_rb_create_domain();
[DllImport("mono.dll")]
internal static extern int mono_rb_unload_domain();
Handle loading of a new dll:
var icalls = new Icalls();
int domaincreation = RustBuster.mono_rb_create_domain();
byte[] bytes = getyourdllsbytearraysomehow;
IntPtr pluginMem = Marshal.AllocHGlobal(bytes.Length);
for (int i = 0; i < bytes.Length; i++)
{
Marshal.WriteByte(pluginMem, i, bytes[i]);
}
assembly = icalls.mono_rb_load_plugin(pluginMem, (uint) bytes.Length);
Unload:
int domaincheck2 = RustBuster.mono_rb_unload_domain();
来源:https://stackoverflow.com/questions/54281115/appdomain-support-is-dead-in-unityengine-any-way-to-unload-dll