Replace Entire Function Bodies with Inline IL in C#

May 10, 2011
This complete utility program, ILFunc, allows entire C# function bodies to be automatically replaced with IL code, where the IL code is specified inline in the C# file, via a CLR custom attribute.

In the following example, the C# function—which gets entirely replaced—happens to use managed pointers, but it could be any C# function. The ILFunc utility always replaces the entire function body. The IL must be compatible with ILASM syntax, and include the .locals directive, if any. The application of the IL code specified in the [ILFuncAttribute] can be switched on and off by surrounding the custom attribute with C# conditional directives.
using System;
using IlFunc;

// etc...

class MyClass
{
		// etc...

		[ILFunc(@"
.locals (
	[0] int32 ix, 
	[1] valuetype agree.arr_tfs_entry& pinned _arr_base, 
	[2] valuetype agree.arr_tfs_entry* _cur)

			ldarg.0
			ldfld      int32[] agree.ArrayTfs::hidx
			ldarg.1
			ldc.i4.s   16
			shl
			ldarg.2
			xor
			ldarg.0
			ldfld      int32[] agree.ArrayTfs::hidx
			ldlen
			rem.un
			conv.u
			ldelem.i4
			dup
			stloc.0
			ldc.i4.m1
			beq.s exit_fail

			ldarg.0
			ldfld      valuetype agree.arr_tfs_entry[] agree.ArrayTfs::entries
			ldc.i4.0
			ldelema    agree.arr_tfs_entry
			stloc.1

loop_start:	ldloc.1
			ldloc.0
			sizeof     agree.arr_tfs_entry
			mul
			add
			dup
			stloc.2
			ldfld      int32 agree.arr_tfs_entry::mark
			ldarg.2
			bne.un.s no_match

			ldloc.2
			ldfld      int32 agree.arr_tfs_entry::i_feat
			ldarg.1
			bne.un.s no_match

			ldarg.3
			ldloc.2
			ldfld      valuetype agree.Edge agree.arr_tfs_entry::e
			stobj      agree.Edge
			ldc.i4.1
			ret

no_match:	ldloc.2
			ldfld      int32 agree.arr_tfs_entry::next
			dup
			stloc.0
			ldc.i4.m1
			bne.un.s loop_start
exit_fail:
			ldarg.3
			ldc.i4.0
			conv.i8
			stind.i8
			ldc.i4.0
			ret
")]
		public unsafe bool TryGetEdge(int i_feat, int mark, out Edge e)
		{
			Debug.Assert(mark != 0);
			int ix;
			if ((ix = hidx[(uint)((i_feat << 16) ^ mark) % (uint)hidx.Length]) != -1)
			{
				fixed (arr_tfs_entry* _pe = entries)
				{
					arr_tfs_entry* pe;
					do
					{
						pe = _pe + ix;
						if (pe->mark == mark && pe->i_feat == i_feat)
						{
							e = pe->e;
							return true;
						}
					}
					while ((ix = pe->next) != -1);
				}
			}
			e = default(Edge);
			return false;
		}
};
Here is the source code for the utility. It is a single file comprising a console executable which can be run as part of your build process. You must add a reference to ILFunc.exe to your project, but only to consume the ILFunc(Attribute) custom attribute. See the instructions in the comment below.

ILFunc works by loading your assembly into the reflection-only context, in order to accurately extract the attribute data (your IL code) and the function(s) it is associated with. This is done on a copy since the disk file of your assembly may need to be overwritten later. It then disassembles your assembly using ILDASM and greps for instances of the custom attribute, matching them to the reflection data. The code bodies are replaced and the resulting IL source is written out and re-assembled, along with any native resources that were disgorged.

Special attention is given to line number information such that you can single-step your IL code in the VS debugger and set breakpoints on individual IL lines, if desired. Pass the /debug command-line switch for this.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace IlFunc
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
    public class ILFuncAttribute : Attribute
    {
        public ILFuncAttribute(String il) { }
    };

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    /// <author>Glenn Slayden</author>
    /// <date>May 9, 2011</date>
    /// <copyright>This is free, unrestricted software. No warranty; as-is.</copyright>
    /// <summary>
    /// ILFunc allows entire C# function bodies to be replaced with IL code, where the IL code is specified inline 
    /// in the C# file, via a CLR custom attribute, ILFuncAttribute. For example:
    /// 
    ///     using ILFunc;
    ///
    /// 	class MyClass
    /// 	{
    /// 		[ILFunc(@"
    /// .locals init ([0] int32 i_arg)
    /// ldc.i4.3
    /// ret
    /// ")]
    /// 		int MyFunc(int i_arg)
    /// 		{
    ///				return 3;
    /// 		}
    ///		};
    ///		
    /// To gain access to the ILFuncAttribute, add a reference to this ILFunc console exe to your project.
    /// 
    /// The C# body is discarded. Like other similar tools, this utility uses the ILDASM/ILASM round-trip capability to 
    /// modify the disassembled source of your library. You will need to have ILDASM.exe (from the Windows7 and .NET 3.5 
    /// SDK) and ILASM.exe (found in your .NET installation at \windows\Microsoft.NET\Framework(64)\v4.0...) in your PATH
    /// environment variable; this program makes no attempt to locate them before invoking them.
    /// 
    /// As a post-build step in VS2010, just call this program with the name of the assembly:
    /// 
    /// ilfunc /$(ConfigurationName) $(TargetPath)
    /// 
    /// This also shows how passing in the optional flag '/Debug' enables single-stepping over IL statements by in the 
    /// debugger by issuing the ILASM switch /debug instead of /debug:opt. This works by writing adjusted line numbers
    /// to a new PDB, if line number information was available in the assembly.
    /// 
    /// ILFunc will overwrite the assembly with a version where the IL code found in the [ILFunc("...")] attribute will 
    /// replace the method body of the corresponding function. Tested on .NET 4.0.
    /// </summary>
    /// <remarks>
    /// If ILDASM produces a win32 resource file, this situation is detected and the file is re-incorporated into the 
    /// new DLL.
    /// The entire C# function body is removed, including the .maxstack directive. This is desirable in order to force
    /// ILASM to recompute .maxstack.
    /// </remarks>
    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public class IlFuncs
    {
        static String path;
        static Type il_attr;

        const String res_file_marker = "// WARNING: Created Win32 resource file";
        const String il_attr_marker = ".custom instance void [IlFunc]IlFunc.ILFuncAttribute::.ctor(string)";
        const String il_end_method_marker = "} // end of method ";
        const String line_marker = ".line ";

        /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        /// <summary>
        /// 
        /// </summary>
        /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        public static void Main(String[] args)
        {
            int arg_ix;
            if ((arg_ix = args.IndexOfFirst(s => s.ToLower() == "/release")) != -1)
                args = args.Where((e, x) => x != arg_ix).ToArray();
            bool f_debug = false;
            if ((arg_ix = args.IndexOfFirst(s => s.ToLower() == "/debug")) != -1)
            {
                f_debug = true;
                args = args.Where((e, x) => x != arg_ix).ToArray();
            }

            if (args.Length != 1)
            {
                Console.WriteLine("Usage: IlFunc assemblyPath");
                return;
            }
            String file_arg = args[0];

            String asm_file_in = Path.GetFullPath(file_arg);
            path = Path.GetDirectoryName(asm_file_in);

            if (!File.Exists(file_arg))
            {
                Console.Error.WriteLine("The file '{0}' could not be found.", file_arg);
                return;
            }

            String ext = asm_file_in.ToLower().EndsWith(".dll") ? "dll" : "exe";

            //////////////////////////////////////////////////////////////////////////////////////////
            /// Make a copy to use for reflection, so we can replace the original later
            //////////////////////////////////////////////////////////////////////////////////////////

            String asm_file_in_copy = Path.GetTempFileName();
            Console.WriteLine("copy {0} {1}", asm_file_in, asm_file_in_copy);
            File.Copy(asm_file_in, asm_file_in_copy, true);

            Dictionary<String, String> inserts = new Dictionary<String, String>();

            //////////////////////////////////////////////////////////////////////////////////////////
            /// Hook the reflection-only assembly resolve event 
            //////////////////////////////////////////////////////////////////////////////////////////
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += new ResolveEventHandler(_resolver);

            //////////////////////////////////////////////////////////////////////////////////////////
            /// Load the specified assembly for reflection, in order to grab the new IL code
            /// from the custom attributes. Then look for methods decorated with ILFuncAttribute 
            /// and extract the attached IL into a dictionary. Despite the fact that the attribute 
            /// requests no inheritance, the GetCustomAttributeData function returns inheriting 
            /// classes too.
            //////////////////////////////////////////////////////////////////////////////////////////
            foreach (Module m in Assembly.ReflectionOnlyLoadFrom(asm_file_in_copy).GetModules())
            {
                foreach (Type t in m.GetTypes())
                {
                    BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic |
									  BindingFlags.Instance | BindingFlags.Static;
                    foreach (MethodInfo mi in t.GetMethods(bf))
                    {
                        var cad = mi.GetCustomAttributesData()
									.Where(x => x.Constructor.ReflectedType == il_attr)
									.FirstOrDefault();
                        if (cad != null)
                        {
                            String type_method = t.Name + "::" + mi.Name;
                            Console.WriteLine("Found IL insert for method {0}", type_method);

                            inserts[type_method] = (String)cad.ConstructorArguments[0].Value;
                        }
                    }
                }
            }

            String src_il_file = Path.GetTempFileName();

            //////////////////////////////////////////////////////////////////////////////////////////
            /// Disassemble the assembly
            //////////////////////////////////////////////////////////////////////////////////////////

            Exec("ildasm", String.Format("\"{0}\" /linenum /out:\"{1}\" /utf8", asm_file_in, src_il_file));

            //////////////////////////////////////////////////////////////////////////////////////////
            /// Read the original IL file and look for the name of a resource file that might
            /// have been written.
            //////////////////////////////////////////////////////////////////////////////////////////

            String win32_res_file = null;
            String il;
            List<line_num_info> rglni = new List<line_num_info>();
            using (StringWriter sw = new StringWriter(new StringBuilder((int)new FileInfo(src_il_file).Length)))
            {
                int line_no = 1;
                foreach (String x in File.ReadLines(src_il_file))
                {
                    if (x.StartsWith(res_file_marker))
                    {
                        win32_res_file = x.Substring(res_file_marker.Length).Trim();
                        Console.WriteLine("(will include Win32 resource file '{0}')", win32_res_file);
                    }
                    if (x.TrimStart().StartsWith(line_marker))
                    {
                        var lni = new line_num_info(line_no, sw.GetStringBuilder().Length, x);
                        rglni.Add(lni);
                    }
                    sw.WriteLine(x);
                    line_no++;
                }
                il = sw.ToString();
            }

            //////////////////////////////////////////////////////////////////////////////////////////
            /// Copy original IL to a new file, replacing functions as we go
            //////////////////////////////////////////////////////////////////////////////////////////

            String new_il;
            bool f_any = false;
            using (StringWriter sw = new StringWriter(new StringBuilder(il.Length)))
            {
                int i = 0, j;
                while ((j = il.IndexOf(il_attr_marker, i)) > i)
                {
                    sw.Write(il.Substring(i, j - i));

                    j = il.IndexOf(il_end_method_marker, j);
                    if (j == -1)
                        throw new Exception();

                    String s_meth = il.Skip(j + il_end_method_marker.Length)
                                        .TakeWhile(ch => !Char.IsWhiteSpace(ch))
                                        .NewString();

                    String repl_code;
                    if (!inserts.TryGetValue(s_meth, out repl_code))
                    {
                        Console.Error.WriteLine("Error locating IL insert for disassembled method '{0}'", s_meth);
                        Environment.Exit(0);
                    }

                    int ins_lineno = -1;
                    if (rglni.Count > 0)
                    {
                        int z = rglni.BinarySearch(j, x => x.offset, (x, y) => x - y);
                        if (z < 0)
                            z = ~z;
                        line_num_info lni = rglni[z];

                        if (File.Exists(lni.file_name))
                        {
                            String code_file = File.ReadAllText(lni.file_name);

                            int co = code_file.Lines()
                                            .Take(lni.start_line)
                                            .StringJoin(Environment.NewLine)
                                            .LastIndexOf(repl_code);

                            int offs = 0;
                            ins_lineno = code_file.Lines()
                                                .TakeWhile(s => (offs += s.Length + Environment.NewLine.Length) < co)
                                                .Count() + 1;
                        }
                    }


                    if (ins_lineno >= 0)
                    {
                        int n1 = 0;
                        foreach (String _il_line in repl_code.Lines())
                        {
                            String il_line = _il_line.TrimEnd();
                            if (n1 == 0 && il_line.Length > 0 && !String.IsNullOrWhiteSpace(il_line))
                            {
                                sw.WriteLine("    .line {0},{0} : {1},{2} ''",
                                                        ins_lineno,
                                                        il_line.Length - il_line.TrimStart().Length + 1,
                                                        il_line.Length + 1);
                            }
                            sw.WriteLine(il_line);
                            ins_lineno++;
                            n1 += il_line.Count(ch => ch == '(') - il_line.Count(ch => ch == ')');
                        }
                    }
                    else
                        sw.WriteLine(repl_code);

                    Console.WriteLine("Replaced method {0}", s_meth);
                    f_any = true;

                    i = j;
                }
                if (il.Length > i)
                    sw.Write(il.Substring(i, il.Length - i));
                new_il = sw.ToString();
            }

            if (!f_any)
            {
                Console.WriteLine("Did not replace any methods. No need to replace the assembly.");
            }
            else
            {
                String new_il_file = Path.GetTempFileName();
                Console.WriteLine("writing new IL file '{0}'", new_il_file);
                File.WriteAllText(new_il_file, new_il, Encoding.UTF8);

                //////////////////////////////////////////////////////////////////////////////////////////
                /// Assemble the modified IL file
                //////////////////////////////////////////////////////////////////////////////////////////

                String new_asm_file = Path.Combine(path, Path.GetFileNameWithoutExtension(asm_file_in) + "." + ext);

                String ilasm_cmd = String.Format("/optimize {0} /quiet /{1} \"{2}\" /out:\"{3}\"",
                                            f_debug ? "/debug" : "/debug:opt",
                                            ext,
                                            new_il_file,
                                            new_asm_file);
                if (win32_res_file != null)
                    ilasm_cmd += String.Format(" /res:\"{0}\"", win32_res_file);

                Exec("ilasm", ilasm_cmd);

                //////////////////////////////////////////////////////////////////////////////////////////
                /// Delete temporary file
                //////////////////////////////////////////////////////////////////////////////////////////
                Console.WriteLine("delete \"{0}\"", new_il_file);
                File.Delete(new_il_file);
            }

            ////////////////////////////////////////////////////////////////////////////////////////////
            ///// Delete temporary files
            ////////////////////////////////////////////////////////////////////////////////////////////
            Console.WriteLine("delete \"{0}\"", src_il_file);
            File.Delete(src_il_file);
            if (win32_res_file != null)
            {
                Console.WriteLine("delete \"{0}\"", win32_res_file);
                File.Delete(win32_res_file);
            }
        }

        /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        /// <summary>
        /// 
        /// </summary>
        /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        static Assembly _resolver(object sender, ResolveEventArgs args)
        {
            AssemblyName an = new AssemblyName(args.Name);
            Assembly a = null;

            try { a = Assembly.ReflectionOnlyLoad(args.Name); }
            catch { }

            if (a == null)
            {
                try { a = Assembly.ReflectionOnlyLoad(an.Name + ".dll"); }
                catch { }
            }

            if (a == null)
            {
                try { a = Assembly.ReflectionOnlyLoadFrom(Path.Combine(path, an.Name + ".dll")); }
                catch { }
            }

            if (new AssemblyName(a.FullName).Name == "IlFunc")
                il_attr = a.GetType("IlFunc.ILFuncAttribute");

            return a;
        }

        /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        /// <summary>
        /// 
        /// </summary>
        /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        static void Exec(string cmd, string args)
        {
            ProcessStartInfo info = new ProcessStartInfo(cmd, args);
            info.CreateNoWindow = false;
            info.UseShellExecute = false;

            Process p = Process.Start(info);
            p.WaitForExit();

            String disp = String.Format("{0} {1}", cmd, args);
            if (p.ExitCode != 0)
                throw new ArgumentException(String.Format("{0} failed with exit code {1}.", disp, p.ExitCode));

            Console.WriteLine(disp + " (ok)");
        }

        [DebuggerDisplay("{il_line} {offset}  -  {start_line},{end_line} : {start_col},{end_col} {file_name}")]
        struct line_num_info
        {
            static String prev_file = null;
            static Char[] split_chars = " ,'".ToArray();
            public line_num_info(int il_line, int offset, String s)
            {
                var rgs = s.Split(split_chars, StringSplitOptions.RemoveEmptyEntries);
                if (rgs.Length > 6)
                    prev_file = rgs[6];

                this.il_line = il_line;
                this.offset = offset;
                this.start_line = int.Parse(rgs[1]);
                this.end_line = int.Parse(rgs[2]);
                this.start_col = int.Parse(rgs[4]);
                this.end_col = int.Parse(rgs[5]);
                this.file_name = prev_file;
            }
            public int il_line;
            public int offset;
            public int start_line;
            public int end_line;
            public int start_col;
            public int end_col;
            public String file_name;
        }
    }

    static class Extensions
    {
        public static String NewString(this IEnumerable<Char> ie)
        {
            return new String(ie.ToArray());
        }

        public static String StringJoin(this IEnumerable<String> ie, String sep)
        {
            return String.Join(sep, ie);
        }

        public static IEnumerable<String> Lines(this String s)
        {
            using (StringReader sr = new StringReader(s))
                while ((s = sr.ReadLine()) != null)
                    yield return s;
        }

        public static int BinarySearch<T, K>(this IList<T> list, K value, Converter<T, K> convert, Comparison<K> compare)
        {
            int i = 0;
            int j = list.Count - 1;
            while (i <= j)
            {
                int m = i + (j - i) / 2;
                int r = compare(convert(list[m]), value);
                if (r == 0)
                    return m;
                if (r < 0)
                    i = m + 1;
                else
                    j = m - 1;
            }
            return ~i;
        }
        public static int IndexOfFirst<TSrc>(this IEnumerable<TSrc> seq, Func<TSrc, bool> predicate)
        {
            int i = 0;
            foreach (TSrc t in seq)
            {
                if (predicate(t))
                    return i;
                i++;
            }
            return -1;
        }
    };
}