Skip to main content

Function Injection Rules

In order to avoid dirty function contagion, a small piece of check jump code is injected into the header of all functions by default. This injected code has a significant impact on the performance of short functions and the length of the final generated code (increased code by about 30%). Although in most cases the impact of injected code on overall performance is negligible, on rare special occasions this performance degradation will be observed. Since version v4.5.9, custom configuration of this injection behavior is allowed.

Dirty function contagion

We call changing functions dirty functions. If no modifications are made to the original code generated by il2cpp, there will be a problem of dirty function chain infection for non-virtual function calls. For example: function A calls function B, Function B calls function C. If function C changes, A, B, and C will all be marked as dirty functions. In practice, changes in some commonly used basic functions may lead to a huge amount of code being marked as dirty functions. This is obviously not what we expected.

classFoo
{

public static void A()
{
B();
}

public static void B()
{
C();
}

public static void C()
{
//The old code is new object();
// After modification, A, B, and C are all marked as dirty functions.
new List<int>();
}
}

Indirect function optimization technology

We use the technique of indirect function optimization to overcome this problem. When il2cpp generates code, it inserts a check code at the head of the DHE function. If the function does not change, execution continues, otherwise it jumps to the interpretation function for execution.

Take the following csharp code as an example:

     public class IndirectChangedNotInjectMethod
{
public static int ChangeMethod10(int x)
{
return ChangeMethod0(x);
}

public static int ChangeMethod100(int x)
{
return ChangeMethod10(x);
}
}

The original il2cpp code generated by the ChangeMethod100 function is as follows:

  IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR int32_t IndirectChangedNotInjectMethod_ChangeMethod100_mFE65234D8ACE343677581C1D96E05E9DFC7C2D1A (int32_t ___0_x, const RuntimeMethod* method)
{
{
// return ChangeMethod10(x);
int32_t L_0 = ___0_x;
int32_t L_1;
L_1 = IndirectChangedNotInjectMethod_ChangeMethod10_m1CFE86C6F8D9E11116BA0F8CACB72A31D4F8401E(L_0, NULL);
return L_1;
}
}

After inserting the check and redirect invoking code, it becomes:


IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR int32_t IndirectChangedNotInjectMethod_ChangeMethod100_mFE65234D8ACE343677581C1D96E05E9DFC7C2D1A (int32_t ___0_x, const RuntimeMethod* method)
{
static bool s_Il2CppMethodInitialized;
if (!s_Il2CppMethodInitialized)
{
il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&IndirectChangedNotInjectMethod_ChangeMethod100_mFE65234D8ACE343677581C1D96E05E9DFC7C2D1A_RuntimeMethod_var);
s_Il2CppMethodInitialized = true;
}
method = IndirectChangedNotInjectMethod_ChangeMethod100_mFE65234D8ACE343677581C1D96E05E9DFC7C2D1A_RuntimeMethod_var;
if (method->isInterpterImpl)
{
typedef int32_t (*RedirectFunc)(int32_t, const RuntimeMethod*);
return ((RedirectFunc)method->methodPointerCallByInterp)(___0_x, method);
}
{
// return ChangeMethod10(x);
int32_t L_0 = ___0_x;
int32_t L_1;
L_1 = IndirectChangedNotInjectMethod_ChangeMethod10_m1CFE86C6F8D9E11116BA0F8CACB72A31D4F8401E(L_0, NULL);
return L_1;
}
}

The injected code contains the following:

  • The metadata initialization block in the header adds the initialization code of the metadata corresponding to the current function. If the function originally does not have any metadata that needs to be initialized, add the entire metadata initialization code block.
  • Added a new branch to check the code. If the current function is replaced by interpreted execution, jump to interpreted execution

For most cases, the injected code only has one additional check if (method->isInterpterImpl), and the impact on overall performance is negligible. But for short functions (such as int GetValue() { return value; }), Since the code of the short function itself is short, there is often no metadata that needs to be initialized, which leads to the introduction of two additional checks and may prevent the function from being inline, resulting in observable performance degradation (10% or more) and significant code bloat. (Added two blocks of code).

Even if it is not a short function, the injected code causes the overall size of the code file generated by the DHE assembly to increase by 30%. This impact on the package body cannot be ignored.

In fact, many short functions will not change. Injecting code is unnecessary. Avoiding injection can significantly improve their performance and reduce the size of the final generated cpp code to a certain extent. For this purpose we introduced an injection policy file to configure this behavior.

Inject policy file

We optimize the performance degradation and code bloat caused by indirect function optimization by configuring some or all functions (use with caution, not recommended!) not to inject. Function injection rules (InjectRules) file is used to achieve this purpose.

tip

Even if a function is marked not to be injected, modifying this function in subsequent hot updates will not cause running errors or execute old logic. It will only cause dirty function infection problems, that is, all functions that directly call this function will be infected. Mark function as dirty.

HybridCLR Settings settings

Fill in the injection policy file path in the InjectRuleFiles field in HybridCLR Settings. The relative path of the file is the project root directory (e.g Assets/InjectRules/DefaultInjectRules.xml).

Allows 0-N configuration policy files to be provided. If there is no configuration policy file, function injection for all DHE assemblies is performed by default.

Configuration rules

The configuration syntax is very similar to link.xml. For a function, if multiple rules match, the last rule takes precedence.

A typical injection policy file is as follows:

<rules>
<assembly fullname="*">
<type fullname="*">
<property name="*"/> All properties are not injected
</type>
</assembly>
<assembly fullname="AssemblyA">
<type fullname="Type1">
<method name="*"/>
</type>
<type fullname="Type2">
<property name="Age*"/>
<property name="Age_3" mode="proxy"/>
<property name="Count" mode="none"/>
<property signature="System.String Name"/>
<method name="Run*"/>
<method name="Run_3" mode="proxy"/>
<method name="Foo"/>
<method signature="System.Int32 Sum(System.Int32,System.Int32)"/>
<method signature="System.Int32 Sum2(System.Int32,System.Int32)"/>
<event name="OnEvent*"/>
<event name="OnEvent_3" mode="proxy"/>
<event name="OnHello"/>
</type>
</assembly>
<assembly fullname="AssemblyB">
<type fullname="*">
<method name="*"/>
</type>
</assembly>
</rules>

rules

The top-level tag is rules, and rules can contain 0-n assembly rules.

NameTypeNullableDescription
assemblychild elementnoassembly rules

assembly

Configure rules for a certain assembly or type of assembly.

NameTypeNullableDescription
fullnamepropertynoThe assembly name without the '.dll' suffix. Support wildcard characters, such as '', 'Unity.', 'MyCustom*' and so on
typechild elementsaretype rules. Can contain 0-N

type

Configure injection rules for a certain type or type. Note that injection rules for generic primitive types are supported, but injection rules for configuring generic instance classes are not supported. For example, you can configure the injection rules of List`1, But the injection rules of List<int> cannot be configured.

  • If a function satisfies multiple rules, the last rule will prevail
  • Property is regarded as two functions: get_{name} and set_{name}, so int Count can also be matched by &lt;method name="get_Count"&gt;
NameTypeNullableDescription
fullnamePropertyNoThe full name of the type. Support wildcard characters, such as '', 'Unity.', 'MyCustom.*.TestType' and so on
methodchild elementisfunction rule
propertychild elementisproperty rule
eventchild elementisevent rule

method

Configure function injection rules.

NameTypeNullableDescription
namepropertynofunction name. Support wildcard characters, such as '', 'Run' and so on
signaturepropertyisa function signature. Supports wildcard characters, such as '', 'System.Int32 (System.Int32)'
modechild elementsareinjection types, valid values ​​are 'none' or 'proxy'. If not filled in or empty, take 'none'

property

Configuration property injection rulesbut. Note that the attribute is treated as two functions: get_{name} and set_{name}, so the getter function get_Count of int Count can also be matched by &lt;method name="get_Count"&gt;.

NameTypeNullableDescription
namepropertynofunction name. Support wildcard characters, such as '', 'Run' and so on
signaturepropertyisa function signature. Supports wildcard characters, such as '*', 'System.Int32 *'
modechild elementsareinjection types, valid values are 'none' or 'proxy'. If not filled in or empty, take 'none'

event

Configure event injection rules. Note that the event is treated as two functions: add_{name} and remove_{name}, so the add function add_OnDone of Action OnDone can also be matched by &lt;method name="add_OnDone"&gt;.

NameTypeNullableDescription
namepropertynofunction name. Support wildcard characters, such as '', 'Run' and so on
signaturepropertyisa function signature. Supports wildcard characters, such as '*', 'Action<System.Int32> On*'
modechild elementsareinjection types, valid values are 'none' or 'proxy'. If not filled in or empty, take 'none'

The injection strategy file needs to be consistent with the App, that is, each independently released App must back up the injection strategy file used at that time. Just like every time you generate a dhao file, you need to use the AOT dll generated when building the App. When generating the dhao file, you must use the injection policy file backed up when building the App. If an error injection strategy file is used, an incorrect dhao file will be generated, which may cause the wrong logic to run or even crash!