Skip to content

Tiny weaver and IL parser for Unity3d using Mono.Cecil

License

Notifications You must be signed in to change notification settings

BeauPrime/TinyIL.Mono

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TinyIL.Mono

Current Version: 0.2.1 Updated 5 June 2024

About

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.

How to Use

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.

IL Opcode Reference

Examples

Generic enum-to-integral conversions

[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);
}

Clearing a block of memory

[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);
    }
}

Accessing internal methods without reflection

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();  
}

Using patch files

[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

Notes

TinyIL currently supports a subset of IL features. This is supplemented by several macros and shortcuts.

Type References

TinyIL supports a subset of IL's type reference grammar.

  • Type references take the usual form of Namespace.Type or Namespace.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
  • 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

Method References

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.

Field References

Fields are referenced using the format typeReference::fieldName.

Variable Declarations

Variables can be declared using the format #var name type. For example, #var tempSwap int32.

Calling Conventions

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 references
    • returnType(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 value
  • calli 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

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

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.

IL Patch Files

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

IL2CPP Considerations

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.

Additional Opcodes

The following opcodes are added for convenience.

  • ldc.u4 [unsigned int32] -> ldc.i4 [int32]
  • ldc.u8 [unsigned int64] -> ldc.i8 [int64]

Unsupported Opcodes

  • ldtoken

Roadmap

  • IL patch file support, allowing IL to be injected from separate file added 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 support
  • calli support added in 0.1.3