使用从IL编译的程序集与.NET Core和Xamarin

7

我更新了一种对我有用的解决方案,请参见问题底部。

背景:
我需要一种评估通用类型大小以计算数组长度以适应特定字节大小的方法。基本上,类似于C/C++提供的sizeof

C#的sizeofMarshal.SizeOf不适合此目的,因为它们有许多限制。

有了这个想法,我编写了一个在IL中启用所需功能的程序集,该程序集通过sizeof操作码实现。我知道它基本上对引用类型求值为IntPtr.Size

我在.NET Standard和Core上复制了这个程序集,并引用了我认为是mscorlib的正确等价项。请注意,IL编译得很好,这个问题与另一个问题有关。

代码:
每个目标框架的头文件:

.NET:(Windows\Microsoft.NET\Framework\v4.0.30319\ilasm.exe)

.assembly extern mscorlib {}

.NET标准:(从nuget提取的ilasm)

.assembly extern netstandard 
{ 
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89)
  .ver 0:0:0:0
}
.assembly extern System.Runtime
{
  .ver 0:0:0:0
}

.NET Core:(与标准版相同的ilasm,尽管我已经测试过两种版本)

.assembly extern System.Runtime
{
  .ver 0:0:0:0
}

来源:

.assembly Company.IL
{
  .ver 0:0:1:0
}
.module Company.IL.dll

// CORE is a define for mscorlib, netstandard, and System.Runtime
.class public abstract sealed auto ansi beforefieldinit 
  Company.IL.Embedded extends [CORE]System.Object
{
  .method public hidebysig specialname rtspecialname instance void 
    .ctor() cil managed 
  {
    .maxstack 8
    ldarg.0
    call instance void [CORE]System.Object::.ctor()
    ret
  }

  .method public hidebysig static uint32 
    SizeOf<T>() cil managed 
  {
    sizeof !!0
    ret
  }
}
问题:
当任何以这种方式编译的dll被.NET Core或Xamarin应用程序引用时,我会收到以下错误信息:

类型“Object”在未被引用的程序集中定义。您必须添加对程序集“System.Runtime,Version = 0.0.0.0,Culture = neutral,PublicKeyToken = null”的引用。

当此类dll被.NET项目或.NET标准库引用,然后再由.NET项目引用时,不会出现此问题。

我已经阅读了无数关于此错误的文章、帖子和代码库,它们涉及不同版本和程序集。通常的解决方案似乎是向目标框架的等效mscorlib显式添加引用(破坏可移植性)。似乎缺乏有关使用IL编译的程序集进行.NET Standard和Core的信息。

据我所知,.NET Standard和Core使用外观(facades)转发类型定义,以便可以通过目标框架的运行时解析它们,从而实现可移植性。

我尝试了以下操作:

  • System.Runtime的显式版本
  • 反编译由C#编译的.NET Core库,使用完全相同的引用。奇怪的是它们似乎针对显式版本(例如.NET Core 2.0中的System.Runtime 4.2)。
  • 使用Emit API动态生成代码。编译到内存似乎可以工作,但不是一个选项,因为我也在针对Xamarin.iOS(仅限AOT)。从磁盘引用这样动态编译的程序集会导致与手动编译它们一样的错误。

更新:
我尝试了Jacek的答案中的解决方案(根据这里的构建说明),但无法配置我的系统以使用构建脚本或VS 2017编译corefx。然而,在查看System.Runtime.CompilerServices.Unsafe的代码后,我发现了一个解决方案。

这可能显而易见,但是我引用了错误版本的System.Runtime

每个目标框架的头文件(从corefx复制):

.NET:

#define CORELIB "mscorlib"

.assembly extern CORELIB {}

.NET标准:

#define CORELIB "System.Runtime"
#define netcoreapp

// Metadata version: v4.0.30319
.assembly extern CORELIB
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )
  .ver 4:0:0:0
}

.NET Core:

#define CORELIB "System.Runtime"

// Metadata version: v4.0.30319
.assembly extern CORELIB
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )
  .ver 4:0:0:0
}

在所有源文件中,使用CORELIB来引用mscorlib中的类型(例如,[CORELIB]System.Object)。

你的 Reflection.Emit 尝试的结果是什么? - thehennyy
@thehennyy 更新了更多关于此事的细节。 - Koby Duck
@KobyDuck:刚刚添加了在项目引用仅限IL程序集时抑制编译错误的选项。这也可能会有所帮助。 - Jacek Blaszczynski
你在这上面浪费时间了,sizeof实际上并不返回对象的大小。它返回对象引用的大小,即4或8个字节。对于值类型来说是可以的。在.NET中无法获取实际大小是非常基本的。 - Hans Passant
@Hans Passant 我明白了。对于我的使用情况来说是可以接受的。 - Koby Duck
@kobyduck,您还可以查看https://github.com/DotNetCross/NativeInts,其中包含完全使用IL编写的代码,并具有可行的解决方案。 - nietras
3个回答

4
在DotNet CoreFX存储库中有一个非常好的示例,展示了如何正确地执行此操作。System.Runtime.CompilerServices.Unsafe是仅限IL的程序集,可供.NET Core和Xamarin使用。

https://github.com/dotnet/corefx/tree/master/src/System.Runtime.CompilerServices.Unsafe

有两种解决问题的方法:(i) 从头开始重新创建构建配置所需的元素 - 这将耗费时间并且非常复杂 - corefx构建系统真的很复杂,(ii) 使用现有的构建基础设施,并通过复制System.Runtime.CompilerServices.Unsafe项目,在其中创建您的IL项目,并更改命名和替换IL代码。在我看来,这是构建基于IL的程序集的最快方法,可以保证在所有目标上正常工作。

要在针对任何特定版本的.NET Core或.NET Standard进行构建您的程序集,只需在发布分支中创建它:release/2.0.0release/1.1.0等。

类型“Object”在未引用的程序集中定义。您必须添加对程序集“System.Runtime,Version=0.0.0.0,Culture=neutral,PublicKeyToken=null”的引用。

有一个选项可以尝试仅在触发该编译错误的项目中引用它以抑制它。将以下属性放入新格式的csproj/vbproj应该可以抑制它:

<PropertyGroup>
    <_HasReferenceToSystemRuntime>true</_HasReferenceToSystemRuntime>
</PropertyGroup>

我非常忙,还没有时间尝试这个。稍后我会试一试并回复你的。谢谢。 - Koby Duck
我无法编译corefx,但是你的答案为我提供了一个可行的解决方案。请查看我的更新问题以获取详细信息。对于那些能够编译corefx的人来说,你的解决方案应该有效。 - Koby Duck
此示例使用未发布的 SDK 'Microsoft.NET.Sdk.IL'。 - Alexei Shcherbakov
不是的。答案是在“Microsoft.NET.Sdk.IL”还不存在的时候编写的。SDK的使用是corefx存储库中在2018年下半年添加的新变化。 - Jacek Blaszczynski

1

好的,这是我研究的结果(基于 https://github.com/dotnet/corefx/tree/master/src/System.Runtime.CompilerServices.Unsafehttps://github.com/jvbsl/ILProj):

global.json:

{
  "msbuild-sdks": {
    "Microsoft.NET.Sdk.IL": "3.0.0-preview-27318-01"
  }
}

ILProj\ILProj.ilproj:

<Project Sdk="Microsoft.NET.Sdk.IL">

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <MicrosoftNetCoreIlasmPackageVersion>3.0.0-preview-27318-01</MicrosoftNetCoreIlasmPackageVersion>
        <IncludePath Condition="'$(TargetFramework)' == 'netstandard1.0'">include\netstandard</IncludePath>
        <IncludePath Condition="'$(TargetFramework)' == 'netstandard2.0'">include\netstandard</IncludePath>
        <IncludePath Condition="'$(TargetFramework)' == 'netcoreapp1.0'">include\netcoreapp</IncludePath>
        <IncludePath Condition="'$(TargetFramework)' == 'netcoreapp2.0'">include\netcoreapp</IncludePath>
        <IlasmFlags>$(IlasmFlags) -INCLUDE=$(IncludePath)</IlasmFlags>
    </PropertyGroup>

</Project>

ILProj\include\netstandard\coreassembly.h:

#define CORE_ASSEMBLY "System.Runtime"

// Metadata version: v4.0.30319
.assembly extern CORE_ASSEMBLY
{
  .publickeytoken = ( B0 3F 5F 7F 11 D5 0A 3A )
  .ver 4:0:0:0
}

ILProj\Class.il

#include "coreassembly.h"

.assembly ILProj
{
  .ver 1:0:0:0
}

.module ILProj.dll

.class public abstract auto ansi sealed beforefieldinit ILProj.Class
  extends [CORE_ASSEMBLY]System.Object
{
  .method public hidebysig static int32 Square(int32 a) cil managed
  {
    .maxstack 2
    ldarg.0
    dup
    mul
    ret
  }
}

如果您在将所有内容组合在一起时遇到困难,可以查看这里的示例:https://github.com/Konard/ILProj

0

我学习了最新的 System.Runtime.CompilerServices.Unsafe 项目,并且创建了可以在多个目标平台上工作的示例(.NET Core 6、.NET Framework 4.8、netstandard2.0、netcoreapp3.1),并使用了两种类型的签名。

你需要创建 global.json 文件

{
    "msbuild-sdks": {
        "Microsoft.NET.Sdk.IL": "6.0.0"
    }
}

然后黑暗魔法开始了:

<Project Sdk="Microsoft.NET.Sdk.IL">
    <PropertyGroup>
        <TargetFrameworks>net6.0;netcoreapp3.1;netstandard2.0;net48</TargetFrameworks>
        <DebugOptimization>IMPL</DebugOptimization>
        <DebugOptimization Condition="'$(Configuration)' == 'Release'">OPT</DebugOptimization>
        <CoreCompileDependsOn>$(CoreCompileDependsOn);GenerateVersionFile</CoreCompileDependsOn>
        <IlasmFlags>$(IlasmFlags) -DEBUG=$(DebugOptimization)</IlasmFlags>
        <IsPackable>true</IsPackable>
        <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
        <AssemblyVersion>1.0.0.0</AssemblyVersion>
        <FileVersion>1.0.0.0</FileVersion>
        <Version>1.0.0-beta.1</Version>
    </PropertyGroup>
    <PropertyGroup Condition="$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)')) == '.NETCoreApp'">
        <ExtraMacros>#define netcoreapp</ExtraMacros>
        <CoreAssembly>System.Runtime</CoreAssembly>
        <_FeaturePublicSign>true</_FeaturePublicSign>
    </PropertyGroup>
    <PropertyGroup Condition="$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)')) == '.NETStandard'">
        <CoreAssembly>netstandard</CoreAssembly>
        <_FeaturePublicSign>true</_FeaturePublicSign>
    </PropertyGroup>
    <PropertyGroup Condition="$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)')) == '.NETFramework'">
        <CoreAssembly>mscorlib</CoreAssembly>
        <_FeaturePublicSign>false</_FeaturePublicSign>
    </PropertyGroup>

    <PropertyGroup Condition="'$(TargetFramework)'=='net6.0'">
        <_FeatureUsePopCount>true</_FeatureUsePopCount>
    </PropertyGroup>
    <PropertyGroup Condition="'$(TargetFramework)'!='net6.0'">
        <_FeatureUsePopCount>false</_FeatureUsePopCount>
    </PropertyGroup>

    <PropertyGroup Condition="'$(_FeaturePublicSign)'=='true'">
        <AssemblyOriginatorKeyFile>..\solar_pub.snk</AssemblyOriginatorKeyFile>
        <PublicSign>True</PublicSign>
    </PropertyGroup>

    <PropertyGroup Condition="'$(_FeaturePublicSign)'!='true'">
        <AssemblyOriginatorKeyFile>..\solar.snk</AssemblyOriginatorKeyFile>
        <DelaySign>false</DelaySign>
    </PropertyGroup>

    <ItemGroup Condition="'$(_FeatureUsePopCount)'=='true'">
        <Compile Include="FlagEnumUtil.PopCount.msil"/>     
    </ItemGroup>
    <ItemGroup Condition="'$(_FeatureUsePopCount)'!='true'">
        <Compile Include="FlagEnumUtil.NoPopCount.msil"/>       
    </ItemGroup>
    

    <ItemGroup>
        <!-- mscorlib is passed in as an explicit reference from C# targets but not via the IL SDK. -->
        <Reference Include="$(CoreAssembly)"
                   Condition="'$(TargetFrameworkIdentifier)' != '.NETStandard'" />
    </ItemGroup>

    <Target Name="GenerateVersionFile"
            DependsOnTargets="GetAssemblyVersion;ResolveReferences"
            Inputs="$(MSBuildAllProjects)"
            Outputs="'$(VersionFilePath)">
        <PropertyGroup>
            <IncludePath>$([MSBuild]::NormalizeDirectory('$(IntermediateOutputPath)', 'version'))</IncludePath>
            <IncludePathTrimmed>$(IncludePath.TrimEnd('\'))</IncludePathTrimmed>
            <IlasmFlags>$(IlasmFlags) -INCLUDE="$(IncludePathTrimmed)"</IlasmFlags>
            <VersionFilePath Condition="'$(VersionFilePath)' == ''">$([MSBuild]::NormalizePath('$(IncludePath)', 'version.h'))</VersionFilePath>
            <_AssemblyVersion>$(AssemblyVersion.Replace('.', ':'))</_AssemblyVersion>
            <_coreAssemblyName Condition="'%(ReferencePath.FileName)' == '$(CoreAssembly)'">%(ReferencePath.FusionName)</_coreAssemblyName>
            <_assemblyNamePattern><![CDATA[[^,]+, Version=(?<v1>[0-9]+)\.(?<v2>[0-9]+)\.(?<v3>[0-9]+)\.(?<v4>[0-9]+), .*PublicKeyToken=(?<p1>[0-9a-f]{2})(?<p2>[0-9a-f]{2})(?<p3>[0-9a-f]{2})(?<p4>[0-9a-f]{2})(?<p5>[0-9a-f]{2})(?<p6>[0-9a-f]{2})(?<p7>[0-9a-f]{2})(?<p8>[0-9a-f]{2})]]></_assemblyNamePattern>
            <_coreAssemblyVersion>
                $([System.Text.RegularExpressions.Regex]::Replace(
                $(_coreAssemblyName),
                $(_assemblyNamePattern),
                '${v1}:${v2}:${v3}:${v4}'))
            </_coreAssemblyVersion>
            <_coreAssemblyVersionTrimmed>$(_coreAssemblyVersion.Trim())</_coreAssemblyVersionTrimmed>
            <_coreAssemblyPublicKeyToken>
                $([System.Text.RegularExpressions.Regex]::Replace(
                $(_coreAssemblyName),
                $(_assemblyNamePattern),
                '${p1} ${p2} ${p3} ${p4} ${p5} ${p6} ${p7} ${p8}').ToUpperInvariant())
            </_coreAssemblyPublicKeyToken>
            <_VersionFileContents>
                <![CDATA[
#define CORE_ASSEMBLY "$(CoreAssembly)"
#define ASSEMBLY_VERSION "$(_AssemblyVersion)"
#define CORE_ASSEMBLY_VERSION "$(_coreAssemblyVersionTrimmed)"
#define FILE_VERSION "{string('$(FileVersion)')}"
#define INFORMATIONAL_VERSION "{string('$(InformationalVersion)')}"
$(ExtraMacros)
// Metadata version: v4.0.30319
.assembly extern CORE_ASSEMBLY
{
  .publickeytoken = ($(_coreAssemblyPublicKeyToken) )
  .ver CORE_ASSEMBLY_VERSION
}
 ]]>
            </_VersionFileContents>
        </PropertyGroup>

        <Message Importance="high" Text="Building:$(TargetFramework) $([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)')) CoreAssembly $(CoreAssembly)#$(_coreAssemblyVersionTrimmed) PublicSign=$(_FeaturePublicSign) PopCount=$(_FeatureUsePopCount)"/>

        <WriteLinesToFile
          File="$(VersionFilePath)"
          Lines="$(_VersionFileContents)"
          Overwrite="true"
          WriteOnlyWhenDifferent="true" />

        <ItemGroup>
            <FileWrites Include="$(VersionFilePath)" />
        </ItemGroup>
    </Target>

    <!-- Decompile the ILResourceReference to get native resources. -->
    <Target Name="SetILResourceReference"
            BeforeTargets="DisassembleIlasmResourceFile"
            Condition="'@(ResolvedMatchingContract)' != ''">
        <ItemGroup>
            <ILResourceReference Include="@(ResolvedMatchingContract)" />
        </ItemGroup>
    </Target>
</Project>

让我们解释一下这个项目文件

  1. DebugOptimization - 我们仅在发布时使用 OPT 优化
  2. CoreCompileDependsOn - 我们需要使用主程序集名称和版本生成 version.h 文件(在目标 GenerateVersionFile 中生成)
  3. IlasmFlags - 用于将 DebugOptimization 传递给 ilasm 工具
  4. IsPackable - 不起作用,但必须创建 nuget 包
  5. ProduceReferenceAssembly - 我们需要禁用 .NET 6 的 ref assembly 搜索
  6. 然后,我们需要为不同的框架定义一些编译时常量
  7. 特殊文件 version.h 看起来像 C++ 头文件,可以包含一些宏,我们用它来掩盖真实的 mscorlib/System.Runtime 程序集名称。.NET 内联代码在 MSBuild 中检测平台并在生成的 version.h 文件中填充宏值(这是在最近完成的)
  8. 我使用两个扩展名来包含不同的源包括规则(所有 *.il 文件都会自动编译,所有 *.msil 文件都是手动包含的)

您可以查看整个项目(包括泛型、枚举和测试):

https://github.com/AlexeiScherbakov/Solar.IL


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接