背景

在有些时候,由于C#的限制,或是追求更高的性能,我们可以编写IL代码来达到我们的目的。本文将介绍几种IL代码开发的几种方式,环境为visual studio 2019 + net5.0 sdk。

本文所用代码均在 https://github.com/huoshan12345/ILDevelopSamples 可以找到

方法1:创建IL项目

项目 System.Runtime.CompilerServices.Unsafe 就是由这种方式编写。

目前,visual studio 2019和dotnet命令并不支持直接创建IL项目,但实际上二者是有相关支持的,所以我们需要“救国一下”。

1.首先我们使用visual studio 2019创建一个空解决方案名为ILDevelopSamples,然后创建一个netstandard2.0的library项目ILDevelopSamples.ILProject,然后关闭解决方案。

2.然后修改该项目的.csproj文件的内容为:

<Project Sdk="Microsoft.NET.Sdk.IL">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<_HasReferenceToSystemRuntime>true</_HasReferenceToSystemRuntime>
</PropertyGroup> <ItemGroup>
<Content Include="**\*.il" />
</ItemGroup>
</Project>

3.将该.csproj文件的扩展名改为.ilproj

4.修改.sln文件内容,将其中对于该项目的引用路径修正,即将其扩展名由.csproj改为.ilproj

5.在解决方案根目录下打开命令行,新建一个文件名为global.json并填写内容为

{
"sdk": {
"version": "5.0",
"rollForward": "latestMajor",
"allowPrerelease": false
},
"msbuild-sdks": {
"Microsoft.NET.Sdk.IL": "5.0.0"
}
}

重新打开解决方案,然后此时vs就可以正常加载该项目了。

6.我们可以添加一个il文件来编写代码了,例如:

.assembly extern mscorlib {}

.assembly ILDevelopSamples.ILProject
{
.ver 1:0:0:0
} .module ILDevelopSamples.ILProject.dll .class public abstract auto ansi sealed beforefieldinit System.IntHelper
{
.method public hidebysig static int32 Square(int32 a) cil managed aggressiveinlining
{
.maxstack 2
ldarg.0
dup
mul
ret
}
}

7.到此,该项目就可以正常编译并被其他.net项目引用了。

方法2:C#项目混合编译IL

这种方式就是通过自定义msbuild的targets,来实现在某个已有的C#项目中添加并编译.il文件,即.cs和.il两者的混合编译。

目前有一款vs的插件对此进行了支持:ILSupport 但这个插件使用了windows平台独有的ildasm.exe和ilasm.exe,所以无法在非windows环境使用,也无法在rider或者使用dotnet命令进行编译。

不过我们可以使用跨平台版本的ildasm/ilasm,并借鉴它的思路,在此感谢这个插件的作者。

1.我们创建一个netstandard2.0的library项目ILDevelopSamples.ILMixed

2.然后在该项目中创建一个新文件il.targets, 并填写以下内容

<?xml version="1.0" encoding="utf-8"?>
<Project> <PropertyGroup>
<_OSPlatform Condition="$([MSBuild]::IsOSPlatform('windows'))">win</_OSPlatform>
<_OSPlatform Condition="$([MSBuild]::IsOSPlatform('linux'))">linux</_OSPlatform>
<_OSPlatform Condition="$([MSBuild]::IsOSPlatform('osx'))">osx</_OSPlatform>
<_OSPlatform Condition="$([MSBuild]::IsOSPlatform('freebsd'))">freebsd</_OSPlatform>
<_OSArchitecture>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)</_OSArchitecture> <MicrosoftNetCoreIlasmPackageRuntimeId Condition="'$(MicrosoftNetCoreIlasmPackageRuntimeId)' == ''">$(_OSPlatform)-$(_OSArchitecture.ToLower())</MicrosoftNetCoreIlasmPackageRuntimeId>
<MicrosoftNETCoreILAsmVersion Condition="'$(MicrosoftNETCoreILAsmVersion)' == ''">5.0.0</MicrosoftNETCoreILAsmVersion>
<MicrosoftNetCoreIlasmPackageName>runtime.$(MicrosoftNetCoreIlasmPackageRuntimeId).microsoft.netcore.ilasm</MicrosoftNetCoreIlasmPackageName>
<MicrosoftNetCoreIldasmPackageName>runtime.$(MicrosoftNetCoreIlasmPackageRuntimeId).microsoft.netcore.ildasm</MicrosoftNetCoreIldasmPackageName> <!-- If ILAsmToolPath is specified, it will be used and no packages will be restored
Otherwise packages will be restored and ilasm and ildasm will be referenced from their packages. -->
<_IlasmDir Condition="'$(ILAsmToolPath)' != ''">$([MSBuild]::NormalizeDirectory($(ILAsmToolPath)))</_IlasmDir>
<_IldasmDir Condition="'$(ILAsmToolPath)' != ''">$([MSBuild]::NormalizeDirectory($(ILAsmToolPath)))</_IldasmDir>
<CoreCompileDependsOn Condition="'$(ILAsmToolPath)' == ''">$(CoreCompileDependsOn);ResolveIlAsmToolPaths</CoreCompileDependsOn>
</PropertyGroup> <ItemGroup Condition="'$(ILAsmToolPath)' == ''">
<_IlasmPackageReference Include="$(MicrosoftNetCoreIlasmPackageName)" Version="$(MicrosoftNETCoreILAsmVersion)" />
<_IlasmPackageReference Include="$(MicrosoftNetCoreIldasmPackageName)" Version="$(MicrosoftNETCoreILAsmVersion)" />
<PackageReference Include="@(_IlasmPackageReference)" ExcludeAssets="native" PrivateAssets="all" IsImplicitlyDefined="true" />
</ItemGroup> <ItemGroup>
<IL Include="**\*.il" Exclude="**\obj\**\*.il;**\bin\**\*.il" />
</ItemGroup> <Target Name="ProcessILAfterCompile" AfterTargets="Compile">
<CallTarget Targets="ResolveIlAsmToolPaths; InitializeIL; CoreDecompile; CoreCompileIL" />
</Target> <Target Name="ResolveIlAsmToolPaths">
<ItemGroup>
<_IlasmPackageReference NativePath="$(NuGetPackageRoot)\%(Identity)\%(Version)\runtimes\$(MicrosoftNetCoreIlasmPackageRuntimeId)\native" />
<_IlasmSourceFiles Include="%(_IlasmPackageReference.NativePath)\**\*" />
</ItemGroup>
<Error Condition="!Exists('%(_IlasmPackageReference.NativePath)')" Text="Package %(_IlasmPackageReference.Identity)\%(_IlasmPackageReference.Version) was not restored" /> <PropertyGroup>
<_IlasmDir Condition="'$(_IlasmDir)' == '' and '%(_IlasmPackageReference.Identity)' == '$(MicrosoftNetCoreIlasmPackageName)'">%(_IlasmPackageReference.NativePath)/</_IlasmDir>
<_IldasmDir Condition="'$(_IldasmDir)' == '' and '%(_IlasmPackageReference.Identity)' == '$(MicrosoftNetCoreIldasmPackageName)'">%(_IlasmPackageReference.NativePath)/</_IldasmDir>
</PropertyGroup>
</Target> <Target Name="InitializeIL">
<PropertyGroup>
<ILFile>@(IntermediateAssembly->'%(RootDir)%(Directory)%(Filename).il', ' ')</ILFile>
<ILResourceFile>@(IntermediateAssembly->'%(RootDir)%(Directory)%(Filename).res', ' ')</ILResourceFile>
<ILFileBackup>@(IntermediateAssembly->'%(RootDir)%(Directory)%(Filename).il.bak', ' ')</ILFileBackup>
<AssemblyFile>@(IntermediateAssembly->'"%(FullPath)"', ' ')</AssemblyFile>
</PropertyGroup>
</Target> <Target Name="CoreDecompile"
Inputs="@(IntermediateAssembly)"
Outputs="$(ILFile)"
Condition=" Exists ( @(IntermediateAssembly) ) ">
<PropertyGroup>
<ILDasm>$(_IldasmDir)ildasm $(AssemblyFile) /OUT="$(ILFile)"</ILDasm>
</PropertyGroup>
<!--<Message Text="$(ILDasm)" Importance="high"/>-->
<Exec Command="$(ILDasm)" ConsoleToMSBuild="true" StandardOutputImportance="Low">
<Output TaskParameter="ExitCode" PropertyName="_IldasmCommandExitCode" />
</Exec>
<Error Condition="'$(_IldasmCommandExitCode)' != '0'" Text="ILDasm failed" />
<Copy SourceFiles="$(ILFile)" DestinationFiles="$(ILFileBackup)" />
<ItemGroup>
<!--MSBuild maintains an item list named FileWrites that contains the files that need to be cleaned.
This list is persisted to a file inside the obj folder that is referred to as the "clean cache."
You can place additional values into the FileWrites item list so that they are removed when the project is cleaned up.-->
<FileWrites Include="$(ILFile)" />
<FileWrites Include="$(ILResourceFile)" />
<FileWrites Include="$(ILFileBackup)" />
</ItemGroup>
<PropertyGroup>
<ILSource>$([System.IO.File]::ReadAllText($(ILFile)))</ILSource>
<Replacement>// method ${method} forwardref removed for IL import</Replacement>
<Pattern>\.method [^{}]+ cil managed forwardref[^}]+} // end of method (?&lt;method&gt;[^ \r\t\n]+)</Pattern>
<ILSource>$([System.Text.RegularExpressions.Regex]::Replace($(ILSource), $(Pattern), $(Replacement)))</ILSource>
<Pattern>\.method [^{}]+ cil managed[^\a]+"extern was not given a DllImport attribute"[^}]+} // end of method (?&lt;method&gt;[^ \r\t\n]+)</Pattern>
<ILSource>$([System.Text.RegularExpressions.Regex]::Replace($(ILSource), $(Pattern), $(Replacement)))</ILSource>
</PropertyGroup>
<WriteLinesToFile File="$(ILFile)" Lines="$(ILSource)" Overwrite="true" />
<PropertyGroup>
<ILSource />
</PropertyGroup>
<Delete Files="@(IntermediateAssembly)" />
</Target> <Target Name="CoreCompileIL"
Inputs="$(MSBuildAllProjects);
@(Compile)"
Outputs="@(IntermediateAssembly)"
Returns=""
DependsOnTargets="$(CoreCompileDependsOn)"> <PropertyGroup>
<_OutputTypeArgument Condition="'$(OutputType)' == 'Library'">-DLL</_OutputTypeArgument>
<_OutputTypeArgument Condition="'$(OutputType)' == 'Exe'">-EXE</_OutputTypeArgument> <_KeyFileArgument Condition="'$(KeyOriginatorFile)' != ''">-KEY="$(KeyOriginatorFile)"</_KeyFileArgument> <_IlasmSwitches>-QUIET -NOLOGO</_IlasmSwitches>
<_IlasmSwitches Condition="'$(FoldIdenticalMethods)' == 'True'">$(_IlasmSwitches) -FOLD</_IlasmSwitches>
<_IlasmSwitches Condition="'$(SizeOfStackReserve)' != ''">$(_IlasmSwitches) -STACK=$(SizeOfStackReserve)</_IlasmSwitches>
<_IlasmSwitches Condition="'$(DebugType)' == 'Full'">$(_IlasmSwitches) -DEBUG</_IlasmSwitches>
<_IlasmSwitches Condition="'$(DebugType)' == 'Impl'">$(_IlasmSwitches) -DEBUG=IMPL</_IlasmSwitches>
<_IlasmSwitches Condition="'$(DebugType)' == 'PdbOnly'">$(_IlasmSwitches) -DEBUG=OPT</_IlasmSwitches>
<_IlasmSwitches Condition="'$(Optimize)' == 'True'">$(_IlasmSwitches) -OPTIMIZE</_IlasmSwitches>
<!--<_IlasmSwitches Condition="'$(IlasmResourceFile)' != ''">$(_IlasmSwitches) -RESOURCES=$(IlasmResourceFile)</_IlasmSwitches>--> <ILAsm>$(_IlasmDir)ilasm $(_IlasmSwitches) $(_OutputTypeArgument) $(IlasmFlags) -OUTPUT=@(IntermediateAssembly->'"%(FullPath)"', ' ') @(IL->'"%(FullPath)"', ' ')</ILAsm>
</PropertyGroup> <!--<Message Text="$(ILAsm)" Importance="high"/>-->
<PropertyGroup Condition=" Exists ( '$(ILFile)' ) ">
<ILAsm>$(ILAsm) "$(ILFile)"</ILAsm>
</PropertyGroup>
<Exec Command="$(ILAsm)">
<Output TaskParameter="ExitCode" PropertyName="_ILAsmExitCode" />
</Exec> <Error Condition="'$(_ILAsmExitCode)' != '0'" Text="ILAsm failed" /> <ItemGroup>
<FileWrites Include="@(IntermediateAssembly->'%(RootDir)%(Directory)DesignTimeResolveAssemblyReferencesInput.cache', ' ')" />
</ItemGroup>
<Touch Files="$(ILFile)" />
</Target> </Project>

3.修改该项目的.csproj文件为

<Project Sdk="Microsoft.NET.Sdk">
<Import Project="il.targets" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>

4.我们可以添加一个il文件来编写代码了,例如:

.assembly extern mscorlib {}

.class public abstract auto ansi sealed beforefieldinit System.ObjectHelper
{
.method public hidebysig static int32 SizeOf<T>() cil managed aggressiveinlining
{
.maxstack 1
sizeof !!0
ret
}
}

5.此时该项目里面的方法即ObjectHelper.SizeOf<T>已经可以被其他项目所使用了。不过,如果我想在本项目中调用这个方法呢?

6.创建与该IL代码中的类System.ObjectHelper同名的C#类,即命名空间为System,类名为ObjectHelper,并填写内容为

using System.Runtime.CompilerServices;

namespace System
{
public class ObjectHelper
{
[MethodImpl(MethodImplOptions.ForwardRef)]
public static extern int SizeOf<T>();
}
}

可以注意到,此处有一个方法和IL代码中的方法签名相同,但是没有body,并且有一个特性即[MethodImpl(MethodImplOptions.ForwardRef)]

7.此时,在本项目中,也可以调用ObjectHelper.SizeOf<T>这个方法了。

以上这些功能得以实现的奥秘就来自于il.targets这个文件。其思路如下:

  • C#项目并不会编译il文件,所以先令这个项目编译为dll
  • 反编译该dll为il文件
  • 将标记为ForwardRef的方法注释掉
  • 将反编译的il文件和项目中原有的il文件一起编译为dll

方法3:使用InlineIL.Fody

InlineIL.Fody这个包能让你在编写C#代码时,能够调用其提供的与IL指令的C#方法,在C#项目编译时,这个库又会将这些方法替换成真正的IL指令。

1.我们创建一个netstandard2.0的library项目ILDevelopSamples.Fody

2.修改该项FodyInlineIL.Fody这两个nuget包。

3.我们创建一个新C#类,然后填写以下内容

using InlineIL;
using static InlineIL.IL.Emit; namespace System
{
public class GenericHelper
{
public static bool AreSame<T>(ref T a, ref T b)
{
Ldarg(nameof(a));
Ldarg(nameof(b));
Ceq();
return IL.Return<bool>();
}
}
}

4.可以看出GenericHelper.AreSame<T>这个方法内部就是调用了一些在InlineIL.IL.Emit命名空间下的方法,它们都分别与一条IL指令对应。

方法4:使用ILGenerator动态构建IL

这种方法就是使用ILGenerator在运行时构建IL代码,例如使用DynamicMethod动态构建一个方法。

和上面三种方法的使用场景有所不同,它适合于那些需要在运行时才能确定的代码。

这个项目 AspectCore,在这方面有广泛引用。这是一款卓越的Aop框架。

总结

本文介绍了三种进行IL代码的方法。它们各自有自己优缺点和应用场景,总结如下

方法 优点 缺点 应用场景
创建IL项目 原生IL 创建的时候较为复杂 较多代码需IL实现
C#项目混合编译IL 原生IL,项目内C#可调用IL方法 项目特别大的时候编译可能会慢 少量方法或类需IL实现 
使用InlineIL.Fody 纯C#编写体验 相比原生IL有极其极其轻微的性能损耗 少量方法或类需IL实现
使用ILGenerator 运行时生成代码,灵活     性能有损耗,需缓存一些对象 需运行时生成代码

有什么问题欢迎一起探讨~~~

【抬杠.NET】如何进行IL代码的开发的更多相关文章

  1. 【抬杠.NET】如何进行IL代码的开发(续)

    背景 之前写了一篇文 [抬杠.NET]如何进行IL代码的开发 介绍了几种IL代码的开发方式. 创建IL项目 C#项目混合编译IL 使用InlineIL.Fody 使用DynamicMethod(ILG ...

  2. 【小白学C#】浅谈.NET中的IL代码

    一.前言 前几天群里有位水友提问:”C#中,当一个方法所传入的参数是一个静态字段的时候,程序是直接到静态字段拿数据还是从复制的函数栈中拿数据“.其实很明显,这和方法参数的传递方式有关,如果是引用传递的 ...

  3. 详解.NET IL代码

    一.前言 IL是什么? Intermediate Language (IL)微软中间语言 C#代码编译过程? C#源代码通过LC转为IL代码,IL主要包含一些元数据和中间语言指令: JIT编译器把IL ...

  4. 用ildasm/ilasm修改IL代码

    原文地址:http://www.cnblogs.com/dudu/archive/2011/05/17/ildasm_ilasm_il.html 在开发中遇到这样一个场景,需要修改一个dll文件(.N ...

  5. 认识IL代码---从开始到现在 <第二篇>

    ·IL代码分析方法 ·IL命令解析 ·.NET学习方法论 1.引言 自从『你必须知道.NET』系列开篇以来,受到大家很多的关注和支持,给予了anytao巨大的鼓励和动力.俱往昔,我发现很多的园友都把目 ...

  6. IL代码

    浅析.NET IL代码   一.前言 IL是什么? Intermediate Language (IL)微软中间语言 C#代码编译过程? C#源代码通过LC转为IL代码,IL主要包含一些元数据和中间语 ...

  7. IL代码完结篇

    读懂IL代码就这么简单(三)完结篇   一 前言 写了两篇关于IL指令相关的文章,分别把值类型与引用类型在 堆与栈上的操作区别详细的写了一遍这第三篇也是最后一篇,之所以到第三篇就结束了,是因为以我现在 ...

  8. 一、源代码-面向CLR的编译器-托管模块-(元数据&IL代码)

    本文脉络图如下: 1.CLR(Common Language Runtime)公共语言运行时简介 (1).公共语言运行时是一种可由多种编程语言一起使用的"运行时". (2).CLR ...

  9. 详解.NET IL代码(一)

    本文主要介绍IL代码,内容大部分来自网上,进行整理合并的. 一.IL简介 为什么要了解IL代码? 如果想学好.NET,IL是必须的基础,IL代码是.NET运行的基础,当我们对运行结果有异议的时候,可以 ...

随机推荐

  1. Mysql优化(出自官方文档) - 第六篇

    Mysql优化(出自官方文档) - 第六篇 目录 Mysql优化(出自官方文档) - 第六篇 Optimizing Subqueries, Derived Tables, View Reference ...

  2. N沟通场效应管深度图解(1)工作原理及Multisim实例仿真

    场效应晶体管(Field Effect Transistor, FET)简称场效应管,是一种由多数载流子参与导电的半导体器件,也称为单极型晶体管,它主要分型场效应管(Junction FET, JFE ...

  3. Ubuntu配置apt安装源为清华源[含自动配置脚本]

    Ubuntu配置apt安装源为清华源[含自动配置脚本] 一.备份原配置文件 Ubuntu 的软件源配置文件是/etc/apt/sources.list.将系统自带的该文件做个备份,以防万一. sudo ...

  4. 最多能创建多少个 TCP 连接?

    我是一个 Linux 服务器上的进程,名叫小进. 老是有人说我最多只能创建 65535 个 TCP 连接. 我不信这个邪,今天我要亲自去实践一下. 我走到操作系统老大的跟前,说: "老操,我 ...

  5. 腾讯互动白板+即时通讯+实时音视频,Android学生端接入

    腾讯互动白板+即时通讯+实时音视频,Android学生端接入 一.简介 线上教学方案:腾讯云互动白板(Tencent Interactive Whiteboard,TIW)+即时通信(Instant ...

  6. elementui——表格的相同内容单元格合并

    在今天工作中遇到了相同单元格需要合并的一个需求,实现记录如下. 实现效果: 任务要求: 对表中体系这一列相同的体系进行合并. 思路:定义一个空数组:[]定义一个变量:0遍历数据如果有相同数据 在空数组 ...

  7. [心得]docker学习笔记

    1. docker是什么??? (1) docker是一台类似虚拟机的功能, 内部由一个个镜像组成, 镜像里可以运行容器, 而这个容器可以是任何东西, 比如mysql, 比如tomcat等等, 它的目 ...

  8. JS高阶函数的使用

    1.何为高阶函数呢? JavaScript的函数其实都指向某个变量.既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数.简单来说,就是对其他 ...

  9. 如何少走弯路安装NLTK?

    NLP中分词是一件麻烦事,nltk可以一定程度上优雅的解决一些需求 如果你去搜索"nltk安装",那么多半会得到以下的代码 import nltk nltk.download() ...

  10. P5816 [CQOI2010]内部白点 题解

    [题目链接] [解析] 好题. 拿到题目首先先看一下它的无解情况是怎么判断的. 然后很明显这个是不存在无解情况的. 因为它的黑点开始都是给定了的,可以理解为一个边界. 而新的变化的黑点不会往外扩张,那 ...