Current Version: 0.2.1 Updated 5 June 2024
TinyIL is a lightweight weaver and IL parser for C# and Unity3d using Mono.Cecil. This allows a developer to access opcodes not otherwise exposed by the language or the accessible libraries by the target platform, as well as generate valid code that cannot be normally generated by the C# compiler. Like with most hand-written bytecode, this is best used for exposing functionality, reducing method size, and performing microoptimizations.
TinyIL scans compiled assemblies for attributes with the name IntrinsicILAttribute
, with a constructor of format IntrinsicILAttribute(string)
. Upon finding these attributes on a method, it parses the attribute's string constructor argument into IL instructions and replaces the contents of the method with those instructions. Instructions are separated by newlines or by semicolons.
TinyIL will also detect methods with attributes named ExternalILAttribute
, with a constructor of format ExternalILAttribute(string)
. These attributes fetch their IL instructions from files with the extension .ilpatch
within the assembly's source file directories. References to these patch files take the form FILENAME:PATCHNAME
, where FILENAME
is the name of the patch file, and PATCHNAME
is the section within the file. For more details on using IL patch files, see IL Patch Files.
[IntrinsicIL("ldarg.0; conv.i4; ret")] // non-boxing conversion
static public int ToInt<T>(T value) where T : struct, Enum
{
// boxing conversion
// c# doesn't let us cast a generic enum to an integer without boxing or type punning
return Convert.ToInt32(value);
}
[IntrinsicIL("ldarg.0; ldc.i4.0; ldarg.1; sizeof !!T; mul; unaligned. 1; initblk; ret")] // using initblk
static public unsafe void ClearBytes<T>(T* start, int length) where T : unmanaged
{
// element-by-element clear
T* end = (start + length);
T* ptr = start;
while(ptr < end) {
*(ptr++) = default(T);
}
}
Note: This requires compiling with Allow unsafe code
checked. Otherwise, calls to this will result in a MethodAccessException
being thrown
public enum SceneLoadState
{
NotLoaded,
Loading,
Loaded,
Unloading
}
[IntrinsicIL("ldarga.s scene; call UnityEngine.SceneManagement.Scene::get_loadingState(); conv.i4; ret;")]
static private SceneLoadState GetLoadingState(Scene scene)
{
throw new NotImplementedException();
}
[ExternalIL("NewBehaviour:FNV_HASH")] // this finds the patch file with name NewBehaviour, and the FNV_HASH section within it
static private unsafe uint FnvHash32(char* ptr, int len) {
throw new NotImplementedException();
}
File: NewBehaviour.ilpatch
== FNV_HASH
// 0=ptr, 1=len
// check for null/empty
ldarg.1
conv.u4
brfalse.s EARLY_EXIT
// variables
#var ptr char*
#var length int32
#var hash uint32
#const BASIS 0x811C9DC5
#const PRIME 16777619
ldarg.0
stloc.0
ldarg.1
stloc.1
ldc.u4 #BASIS
stloc.2
LOOP:
ldloc.0
dup
ldind.i2
ldloc.2
xor
ldc.i4 #PRIME
conv.u4
mul
stloc.2
// increment ptr
ldc.i4.2
add
stloc.0
ldloc.1
ldc.i4.1
sub
dup
stloc.1
brtrue.s LOOP
ldloc.2
br.s REAL_EXIT
EARLY_EXIT:
ldc.i4.0
REAL_EXIT:
ret
TinyIL currently supports a subset of IL features. This is supplemented by several macros and shortcuts.
TinyIL supports a subset of IL's type reference grammar.
- Type references take the usual form of
Namespace.Type
orNamespace.Type+NestedType
- Primitive types can be referenced with these shortcuts (case insensitive)
int64
,int32
,int16
,int8
uint64
,uint32
,uint16
,uint8
float
,double
string
,char
bool
,intptr
,uintptr
object
,void
- The method's Declaring type can be referenced with
[declaringType]
- The method's Parameter types can be referenced with
[param parameterName]
or[arg parameterName]
- The method's Variable types can be referenced with
[var variableName]
- Generic type references have limited support currently
- Use
!!T
and the like for referencing generic types directly - If the type is available through a parameter, use a parameter type reference
- Use
- Pointer type references can be made by appending
*
to another type reference - Pinned type references can be made by appending
pinned
to another type reference- This is only useful for pinned local variables
Methods are referenced using the format typeReference::methodName(typeReference, typeReference, ...)
. The current restrictions on generic type references still apply, and parameter-less generic methods are not currently supported.
Fields are referenced using the format typeReference::fieldName
.
Variables can be declared using the format #var name type
. For example, #var tempSwap int32
.
The calli
opcode requires a Call Site for its operand. This can be formatted in one of two ways: calli signature
and calli convention|signature
.
signature
follows a similar format to method referencesreturnType(paramType, paramType, ...)
convention
, when provided, must be one of the following (case insensitive)default
(default value if no convention is provided)c
,cdecl
stdcall
thiscall
fastcall
vararg
generic
Some examples:
calli void()
will invoke a function pointer with the default calling convention, no arguments, and no return valuecalli c|int32()
will invoke a function pointer with the C calling convention, no arguments, and an Int32 return value.calli uint32(string)
will invoke a function pointer with the default calling convention, a string argument, and a UInt32 return value.
Labels can be declared using the format labelName:
. For example, EarlyExitLabel:
. This will point to the instruction on the following line.
All branching operations must be provided these label names as operands. In the case of switch
, label names must be in a comma-separated list.
Constants can be declared using the format #const name value
. For example, #const PRIME 16777619
.
These constants can then be used as operands on later lines with the format opcode #CONSTNAME
. For example, ldc.i4 #PRIME
.
Using external .ilpatch
files in conjunction with ExternalILAttribute
can be useful for longer hand-coded IL functions, or those requiring labels or more complex flow control.
These files are separated into sections, marked by a == YOUR_PATCH_NAME
header and followed by line-separated or semicolon-separated IL instructions.
Comments are also supported on non-header lines, preceded by //
.
== SOME_PATCH
// patch contents are below
// converts the 0th argument to a ulong.
ldarg.0
conv.u8
ret
== ANOTHER_PATCH
// another patch
ldarg.0
ldarg.1
add
ret
The following opcodes may not be supported by Unity's IL2CPP transpiler
arglist
mkrefany
refanytype
refanyval
IL2CPP fundamentally transforms your code into C++. Significant deviations from standard flow control structures may result in less performant code overall.
The following opcodes are added for convenience.
ldc.u4 [unsigned int32]
->ldc.i4 [int32]
ldc.u8 [unsigned int64]
->ldc.i8 [int64]
ldtoken
IL patch file support, allowing IL to be injected from separate fileadded in 0.1.3- Evaluate switching to hooking into
IPostBuildPlayerScriptDLLs
instead - Support for user-defined processors for basic weaving
- Full support for type reference grammar
- Full support for parameter-less generic method references
ldtoken
supportadded in 0.1.3calli
support