Una solución de actualización en caliente de código que hace época Análisis de flujo de código fuente de huatuo

prefacio

Anteayer, escuché que nació una solución de actualización en caliente llamada huatuo . Realiza actualizaciones en caliente en el código de usuario basado en IL2CPP. Pensé que implementaría un conjunto completo de tiempos de ejecución de máquinas virtuales como algunas soluciones de actualización en caliente, pero después de leer el código fuente, I La expresión estaba muy sorprendida, y al mismo tiempo admiraba el circuito cerebral del autor, que es simplemente maravilloso.

Conceptos básicos de actualización en caliente

Primero debemos comprender los dos backends de secuencias de comandos de Mono e IL2Cpp

El primero es Mono, la estructura es bastante simple, es decir, Mono VM interpreta y ejecuta código IL

C++Luego está IL2CPP, que es relativamente complicado. Necesita traducir IL a código C++, y luego usar el tiempo de ejecución de la máquina virtual IL2CPP para ejecutar nuestro código traducido después de ser compilado por un compilador local C++. El tiempo de ejecución de IL2CPP ha hecho bastante. Incluyendo pero no limitado a C#la reflexión admitida, GC, genéricos, funciones virtuales, interfaz, gestión de subprocesos, etc., para que C# realmente pueda ejecutarse en la máquina virtual IL2CPP

Aparte de la prohibición de las políticas JIT del IOS y de algunas plataformas host, C++IL2CPP en sí mismo es AOT debido a restricciones de idioma, por lo que C#operaciones como LoadAssembly, Emit y Expression, que originalmente eran compatibles con JIT, no son compatibles con IL2CPP. razón por la que necesitamos varias soluciones de actualización de código en caliente

huatuo

Introducción

huatuo es una solución de actualización en caliente de C# casi perfecta con características completas, costo cero, alto rendimiento y poca memoria .

huatuo proporciona un tiempo de ejecución CLR multiplataforma muy completo, que puede ejecutarse de manera eficiente en modo híbrido AOT+intérprete no solo en la plataforma Android, sino también en IOS, consolas y otras plataformas que limitan JIT.

características de Huatuo:

  • Funciones completas. Una implementación casi completa de la especificación ECMA-335 , todas las funciones son compatibles excepto las descritas en "Limitaciones y advertencias" a continuación.
  • Cero costes de aprendizaje y uso. huatuo es un tiempo de ejecución CLR completo, el código de actualización en caliente funciona a la perfección con el código AOT. No es necesario escribir ningún código especial, no hay generación de código y no hay restricciones especiales. La clase de secuencia de comandos y la clase AOT están en el mismo tiempo de ejecución, incluso los códigos como la reflexión y los subprocesos múltiples (volátil, ThreadStatic, Task, asíncrono) pueden funcionar normalmente.
  • Ejecutar de manera eficiente. Implementado un intérprete de registro extremadamente eficiente, todos los indicadores son significativamente mejores que otros esquemas de actualización en caliente. informe de prueba de rendimiento
  • Memoria eficiente. Las clases definidas en la secuencia de comandos de actualización activa ocupan el mismo espacio de memoria que las clases ordinarias de C#, lo que es muy superior a otras soluciones de actualización activa. informe de uso de memoria
  • Soporte nativo para reparar algunos códigos AOT. Sin gastos adicionales de desarrollo y tiempo de ejecución.

Más específicamente, huatuo ha hecho lo siguiente:

  • Implementó una biblioteca de análisis de metadatos (dll) eficiente
  • Se modificó el módulo de gestión de metadatos de il2cpp para realizar el registro dinámico de metadatos.
  • Implementó un compilador de un conjunto de instrucciones IL a un conjunto de instrucciones de registro personalizado
  • Implementado un intérprete de registro eficiente
  • Se proporciona una gran cantidad de funciones de instinto para mejorar el rendimiento del intérprete.
  • Proporcionar compatibilidad con hotfix AOT (en curso)

instalación y uso

https://focus-creative-games.github.io/huatuo/start_up/

infraestructura

El principio básico de huatuo es muy simple. Para el código AOT, use IL2CPP para ejecutar, y para el código que no es AOT (como el dll que proporcionamos), usa huatuo para interpretar y ejecutar.

Ejecutar el diagrama de flujo

Primero mire un diagrama de flujo grande, incluidos los procesos Init, LoadAssembly y Execute de huatuo, donde el lado izquierdo es la llamada de capa IL2CPP y el lado derecho es la llamada de capa huatuo

Ejecutar el diagrama de flujo

En eso

Llamado por Unity Native, ejecutado bool Runtime::Inity luego llamado hastavoid ModuleManager::Initialize

C++

void InterpreterModule::Initialize()
{
    
    
	for (size_t i = 0; ; i++)
	{
    
    
		NativeCallMethod& method = g_callStub[i];
		if (!method.signature)
		{
    
    
			break;
		}
		s_calls.insert_or_assign(method.signature, method);
	}
	for (size_t i = 0; ; i++)
	{
    
    
		NativeInvokeMethod& method = g_invokeStub[i];
		if (!method.signature)
		{
    
    
			break;
		}
		s_invokes.insert_or_assign(method.signature, method);
	}
}

Entre ellos, g_callStub y g_invokeStub son las funciones de instinto proporcionadas por huatuo

C++

NativeCallMethod huatuo::interpreter::g_callStub[] = 
{
    
    
	{
    
    "v", (Il2CppMethodPointer)__Native2ManagedCall_v, (Il2CppMethodPointer)__Native2ManagedCall_AdjustorThunk_v, __Managed2NativeCall_v},
	{
    
    "vi", (Il2CppMethodPointer)__Native2ManagedCall_vi, (Il2CppMethodPointer)__Native2ManagedCall_AdjustorThunk_vi, __Managed2NativeCall_vi},
	{
    
    "vf", (Il2CppMethodPointer)__Native2ManagedCall_vf, (Il2CppMethodPointer)__Native2ManagedCall_AdjustorThunk_vf, __Managed2NativeCall_vf},
	{
    
    "vii", (Il2CppMethodPointer)__Native2ManagedCall_vii, (Il2CppMethodPointer)__Native2ManagedCall_AdjustorThunk_vii, __Managed2NativeCall_vii},
	{
    
    "vfi", (Il2CppMethodPointer)__Native2ManagedCall_vfi, (Il2CppMethodPointer)__Native2ManagedCall_AdjustorThunk_vfi, __Managed2NativeCall_vfi},
	....
}
 
NativeInvokeMethod huatuo::interpreter::g_invokeStub[] = 
{
    
    
	{
    
    "v", __Invoke_instance_v, __Invoke_static_v},
	{
    
    "vi", __Invoke_instance_vi, __Invoke_static_vi},
	{
    
    "vf", __Invoke_instance_vf, __Invoke_static_vf},
	{
    
    "vii", __Invoke_instance_vii, __Invoke_static_vii},
	{
    
    "vfi", __Invoke_instance_vfi, __Invoke_static_vfi},
	{
    
    "vif", __Invoke_instance_vif, __Invoke_static_vif},
	{
    
    "vff", __Invoke_instance_vff, __Invoke_static_vff},
    ...
}

Aquí tomamos la función de uso común __Invoke_instance_v como ejemplo. Podemos ignorar algunos datos relacionados con la construcción del marco de pila, pero debemos prestarle atención. Interpreter::ExecuteEste es el punto de partida de la interpretación y ejecución formal de huatuo. Continuaremos mencionándolo más tarde

C++

static void* __Invoke_static_v(Il2CppMethodPointer methodPtr, const MethodInfo* method, void* __this, void** __args)
{
    
    
    StackObject args[1] = {
    
     };
    ConvertInvokeArgs(args, method, __args);
    StackObject* ret = nullptr;
     Important 
    Interpreter::Execute(method, args, ret);
     Important 
    return nullptr;
}

LoadAssemblyLoadAssembly

También iniciado por Unity Native, el punto de partida es la capa IL2CPP const Il2CppAssembly* Assembly::Load, y luego se llamará a la capa huatuo Il2CppAssembly* Assembly::Createpara analizar la DLL y generar la información de imagen, ensamblaje y metadatos que necesitan IL2CPP y huatuo. El autor implementa el análisis de Assembly en huatuo. Es bastante básico. Se puede considerar como una reproducción uno a uno del comportamiento de análisis de CLR de Dll. En este proceso, por supuesto, se requieren algunas explicaciones. on 很多元数据类型都是直接使用的IL2CPP中的(比如Il2CppTypeDefinition,Il2CppMethodDefinition等),这也是为了与IL2CPP直接Hook联调打下基础huatuo Ejecute la operación, por lo que también se generarán algunos metadatos específicos de huatuo (como TbMethod, MethodBody, ilcodedata pointer, etc., muchos de los cuales también envuelven el tipo de metadatos IL2CPP)

Lo más importante es la estructura de los metadatos del método, puede consultar la Tabla de métodos del CLR, que es casi la misma.

Tabla de métodos de CLR

Ejecutar

Tomemos como ejemplo la demostración oficial. Cuando la función LoadDll.csdel script es llamada por la función LoadGameDll APP.csdel script APP.Main, la pila es la siguiente

C++

 	GameAssembly.dll!il2cpp::vm::SetupMethodsLocked(Il2CppClass * klass, const il2cpp::os::FastAutoLock & lock)1192	C++
 	GameAssembly.dll!il2cpp::vm::Class::SetupMethods(Il2CppClass * klass)1279	C++
 	GameAssembly.dll!huatuo::metadata::GetMethodInfoFromMethodDef(const Il2CppType * type, const Il2CppMethodDefinition * methodDef)572	C++
 	GameAssembly.dll!huatuo::metadata::GetMethodInfo(const Il2CppType * containerType, const Il2CppMethodDefinition * methodDef, const Il2CppGenericInst * instantiation, const Il2CppGenericContext * genericContext)1076	C++
 	GameAssembly.dll!huatuo::metadata::ReadMethodInfoFromToken(huatuo::metadata::Image & image, const Il2CppGenericContainer * klassGenericContainer, const Il2CppGenericContainer * methodGenericContainer, const Il2CppGenericContext * genericContext, Il2CppGenericInst * genericInst, huatuo::metadata::TableType tableType, unsigned int rowIndex)1135	C++
 	GameAssembly.dll!huatuo::metadata::Image::GetMethodInfoFromToken(unsigned int token, const Il2CppGenericContainer * klassGenericContainer, const Il2CppGenericContainer * methodGenericContainer, const Il2CppGenericContext * genericContext)1195	C++
 	GameAssembly.dll!huatuo::transform::HiTransform::Transform(huatuo::metadata::Image * image, const MethodInfo * methodInfo, huatuo::metadata::MethodBody & body, huatuo::interpreter::InterpMethodInfo & result)2405	C++
 	GameAssembly.dll!huatuo::interpreter::InterpreterModule::GetInterpMethodInfo(huatuo::metadata::Image * image, const MethodInfo * methodInfo)332	C++
 	GameAssembly.dll!huatuo::interpreter::Interpreter::Execute(const MethodInfo * methodInfo, huatuo::interpreter::StackObject * args, huatuo::interpreter::StackObject * ret)888	C++
>	GameAssembly.dll!__Invoke_static_i(void(*)() methodPtr, const MethodInfo * method, void * __this, void * * __args)22127	C++
 	GameAssembly.dll!il2cpp::vm::Runtime::Invoke(const MethodInfo * method, void * obj, void * * params, Il2CppException * * exc)575	C++
 	GameAssembly.dll!il2cpp::vm::InvokeConvertThis(const MethodInfo * method, void * thisArg, void * * convertedParameters, Il2CppException * * exception)684	C++
 	GameAssembly.dll!il2cpp::vm::Runtime::InvokeConvertArgs(const MethodInfo * method, void * thisArg, Il2CppObject * * parameters, int paramCount, Il2CppException * * exception)778	C++
 	GameAssembly.dll!il2cpp::vm::Runtime::InvokeArray(const MethodInfo * method, void * obj, Il2CppArray * params, Il2CppException * * exc)594	C++
 	GameAssembly.dll!il2cpp::icalls::mscorlib::System::Reflection::MonoMethod::InternalInvoke(Il2CppReflectionMethod * method, Il2CppObject * thisPtr, Il2CppArray * params, Il2CppException * * exc)240	C++
 	GameAssembly.dll!MonoMethod_InternalInvoke_mFF7E631020CDD3B1CB47F993ED05B4028FC40F7E(MonoMethod_t * __this, Il2CppObject * ___obj0, ObjectU5BU5D_tC1F4EE0DB0B7300255F5FD4AF64FE4C585CF5ADE * ___parameters1, Exception_t * * ___exc2, const MethodInfo * method)39012	C++
 	GameAssembly.dll!MonoMethod_Invoke_mD6E222F8DAB5483E6640B8E399A56B366635B923(MonoMethod_t * __this, Il2CppObject * ___obj0, int ___invokeAttr1, Binder_t2BEE27FD84737D1E79BC47FD67F6D3DD2F2DDA30 * ___binder2, ObjectU5BU5D_tC1F4EE0DB0B7300255F5FD4AF64FE4C585CF5ADE * ___parameters3, CultureInfo_t1B787142231DB79ABDCE0659823F908A040E9A98 * ___culture4, const MethodInfo * method)39080	C++
 	GameAssembly.dll!VirtFuncInvoker5<Il2CppObject *,Il2CppObject *,int,Binder_t2BEE27FD84737D1E79BC47FD67F6D3DD2F2DDA30 *,ObjectU5BU5D_tC1F4EE0DB0B7300255F5FD4AF64FE4C585CF5ADE *,CultureInfo_t1B787142231DB79ABDCE0659823F908A040E9A98 *>::Invoke(unsigned short slot, Il2CppObject * obj, Il2CppObject * p1, int p2, Binder_t2BEE27FD84737D1E79BC47FD67F6D3DD2F2DDA30 * p3, ObjectU5BU5D_tC1F4EE0DB0B7300255F5FD4AF64FE4C585CF5ADE * p4, CultureInfo_t1B787142231DB79ABDCE0659823F908A040E9A98 * p5)71	C++
 	GameAssembly.dll!MethodBase_Invoke_m5DA5E74F34F8FFA8133445BAE0266FD54F7D4EB3(MethodBase_t * __this, Il2CppObject * ___obj0, ObjectU5BU5D_tC1F4EE0DB0B7300255F5FD4AF64FE4C585CF5ADE * ___parameters1, const MethodInfo * method)18289	C++
 	GameAssembly.dll!LoadDll_RunMain_mEDAF0764CCCFDE2F0B9801051CCD5FFDF0241B4C(LoadDll_tF4302664700CA4FCBC0471B8C95631AE6442BC68 * __this, const MethodInfo * method)41550	C++
 	GameAssembly.dll!LoadDll_Start_m9DCFAB46D91AA07BA3DD00B2F19473B66E1E78DB(LoadDll_tF4302664700CA4FCBC0471B8C95631AE6442BC68 * __this, const MethodInfo * method)41413	C++
 	GameAssembly.dll!RuntimeInvoker_TrueVoid_t700C6383A2A510C2CF4DD86DABD5CA9FF70ADAC5(void(*)() methodPointer, const MethodInfo * methodMetadata, void * obj, void * * args)216780	C++
 	GameAssembly.dll!il2cpp::vm::Runtime::Invoke(const MethodInfo * method, void * obj, void * * params, Il2CppException * * exc)575	C++
 	GameAssembly.dll!il2cpp_runtime_invoke(const MethodInfo * method, void * obj, void * * params, Il2CppException * * exc)1118	C++
 	[外部代码]	
 	huatuo.exe!wWinMain(HINSTANCE__ * hInstance, HINSTANCE__ * hPrevInstance, wchar_t * lpCmdLine, int nShowCmd)16	C++
 	[外部代码]	

En particular, cabe señalar que il2cpp::vm::SetupMethodsLockedse 这个函数会将MethodInfo的methodPointer和invoker_method绑定好(我们初始化时构造的那些s_calls和s_invokes)usa para enganchar a huatuo para interpretación y ejecución.

Enlace de puntero de función en MethodInfo

puntero de método:

C++

Il2CppMethodPointer il2cpp::vm::MetadataCache::GetMethodPointer(const Il2CppImage* image, uint32_t token)
{
    
    
    uint32_t rid = GetTokenRowId(token);
    uint32_t table =  GetTokenType(token);
    if (rid == 0)
        return NULL;
 
    // === huatuo
    if (huatuo::metadata::IsInterpreterImage(image))
    {
    
    
        return huatuo::metadata::MetadataModule::GetMethodPointer(image, token);
    }
    // === huatuo
 
    IL2CPP_ASSERT(rid <= image->codeGenModule->methodPointerCount);
 
    return image->codeGenModule->methodPointers[rid - 1];
}

método_invocador:

C++

InvokerMethod il2cpp::vm::MetadataCache::GetMethodInvoker(const Il2CppImage* image, uint32_t token)
{
    
    
    uint32_t rid = GetTokenRowId(token);
    uint32_t table = GetTokenType(token);
    if (rid == 0)
        return NULL;
    // === huatuo
    if (huatuo::metadata::IsInterpreterImage(image))
    {
    
    
        return huatuo::metadata::MetadataModule::GetMethodInvoker(image, token);
    }
    // === huatuo
    int32_t index = image->codeGenModule->invokerIndices[rid - 1];
 
    if (index == kMethodIndexInvalid)
        return NULL;
 
    IL2CPP_ASSERT(index >= 0 && static_cast<uint32_t>(index) < s_Il2CppCodeRegistration->invokerPointersCount);
    return s_Il2CppCodeRegistration->invokerPointers[index];
}

Resumen lateral de IL2CPP

En otros casos, la pila puede ser diferente del ejemplo aquí, pero el núcleo es el mismo:

  1. Llamado por Unity Native
  2. La ejecución il2cpp::vm::SetupMethodsLockedconstruye punteros de función en MethodInfo (si es necesario)
  3. huatuo::interpreter::Interpreter::Executeexplicar la ejecución
  4. (No necesariamente) Llame a la función IL2CPP para completar la realización de la función. Para algunas operaciones matemáticas simples e implementaciones personalizadas de huatuo lógico, si algunas funciones IL2CPP han proporcionado la interfaz correspondiente, entonces llame a la interfaz IL2CPP

Intérprete::Ejecutar

Esta parte se ha descrito en detalle en el diagrama de flujo. Esta función es el núcleo de la interpretación y ejecución de huatuo, y hay alrededor de 7k líneas de invocación de interruptor.

C++

void Interpreter::Execute(const MethodInfo* methodInfo, StackObject* args, StackObject* ret)
{
    
    
	INIT_CLASS(methodInfo->klass);
	MachineState& machine = InterpreterModule::GetCurrentThreadMachineState();
	InterpFrameGroup interpFrameGroup(machine);
	const InterpMethodInfo* imi;
	InterpFrame* frame;
	StackObject* localVarBase;
	byte* ipBase;
	byte* ip;
	PREPARE_NEW_FRAME(methodInfo, args, ret, false);
	// exception handler
	Il2CppException* curException = nullptr;
LoopStart:
	try
	{
    
    
		Execute Interpreter ... 
	}
	catch (Il2CppExceptionWrapper ex)
	{
    
    
		curException = ex.ex;
		PREPARE_EXCEPTION();
		FIND_NEXT_EX_HANDLER_OR_UNWIND();
	}
	catch (Il2CppException* ex)
	{
    
    
		curException = ex;
		PREPARE_EXCEPTION();
		FIND_NEXT_EX_HANDLER_OR_UNWIND();
	}
	return;
UnWindFail:
	IL2CPP_ASSERT(curException);
	interpFrameGroup.CleanUpFrames();
	il2cpp::vm::Exception::Raise(curException);
}

En PREPARE_NEW_FRAME se obtendrá primero el MethodInfo que pueda ser reconocido por huatuo InterpreterModule::GetInterpMethodInfo, si ya hay caché se devolverá directamente sin escape.

C++

InterpMethodInfo* InterpreterModule::GetInterpMethodInfo(metadata::Image* image, const MethodInfo* methodInfo)
{
    
    
	il2cpp::os::FastAutoLock lock(&il2cpp::vm::g_MetadataLock);
	if (methodInfo->huatuoData)
	{
    
    
		return (InterpMethodInfo*)methodInfo->huatuoData;
	}
	metadata::MethodBody& originMethod = image->GetMethodBody(methodInfo->token);
	InterpMethodInfo* imi = new (IL2CPP_MALLOC_ZERO(sizeof(InterpMethodInfo))) InterpMethodInfo;
	transform::HiTransform::Transform(image, methodInfo, originMethod, *imi);
	il2cpp::os::Atomic::FullMemoryBarrier();
	const_cast<MethodInfo*>(methodInfo)->huatuoData = imi;
	return imi;
}

Si no hay caché transform::HiTransform::Transform, traduce y almacena en caché los códigos traducidos en il2cpp-class-internals::MethodInfo.huatuoData

otro

Depuración de código fuente

https://focus-creative-games.github.io/huatuo/source_inspect/

Los puntos de interrupción se pueden rastrear fácilmente hasta el proceso de ejecución del script después de exportar el paquete.

Resumir

Podemos ver que, de hecho huatuo自身并没有一个完整的虚拟机系统,而是借由Unity Native和IL2CPP本身驱动执行自己的一套简单的解释执行栈帧,并且由于其自身就是IL2CPP的拓展,所以跨域调用性能也很强劲(毕竟只是几次指针跳转和函数调用), C++se puede obtener un rendimiento bastante potente mediante compensaciones de puntero y llamadas a funciones.

¿Por qué es una solución de actualización en caliente de código que hace época?

Considere cómo la gran mayoría de los proyectos comerciales están sufriendo actualmente

  • Es más probable que un proyecto mezcle varios idiomas para calor, Lua, C#TS, y se debe implementar un marco en cada idioma. La razón es que el rendimiento de las llamadas entre dominios es demasiado feo o solo se puede hacer de esta manera.
  • Si desea conectar un complemento de terceros que puede implicar actualizaciones en caliente, para que sea estable y utilizable, no debe quedarse sin ese día-hombre de una semana, como Protobuf, debe modificar especialmente su fuente código para adaptarse al marco de actualización en caliente de C # si lo conecta a la C#capa de actualización en caliente La implementación de varias redirecciones solo puede obtener un complemento que todavía está temblando y debe protegerse como un bebé y no atreverse a usarlo casualmente, por no hablar de Lua, la herramienta de anotación lua-protobuf por sí sola es suficiente para que la gente beba una olla, y también hay que modificar y personalizar el código fuente según las necesidades del proyecto. ¿Cuál es el quid del problema? Debido a que estos programas de actualización en caliente están separados del tiempo de ejecución de CLR, CLR no sabe que existe dicha información adicional, por lo que solo se puede construir y mantener artificialmente.
  • C#Las características de alto nivel son obedientes y no se atreven a usar, por temor a alarmar a ese adulto.
  • Las llamadas entre dominios y la herencia entre dominios, estas dos funciones originalmente bastante naturales, requieren que los desarrolladores presten atención a muchos detalles, redirección y generación de código; de lo contrario, será abrumador para GC y Caton.
  • Después de todo, Lua y C# son cosas en diferentes dominios ¿ p/invokeCuánto espacio hay para que los desarrolladores marquen una gran diferencia después de todo tipo de problemas?
  • Especialmente criticando a Lua por su nombre, ¿realmente se puede usar esta cosa como un marco de juego y negocio? Reescribir es más rápido que refactorizar. Otro hermano se hizo cargo y lo envió directamente.

Y estos no son un problema para huatuo, porque admite la actualización en caliente desde la capa inferior.La expansión de Huatuo de IL2CPP hace que Unity Native vea que nuestro código de actualización en caliente y el código AOT son los mismos, es decir, incluso si ejecutamos el backend de IL2CPP , También se puede escribir como una PC o Android bajo el backend Mono.

Supongo que te gusta

Origin blog.csdn.net/m0_58523831/article/details/129727781
Recomendado
Clasificación