이 페이지에는 자동 번역된 텍스트가 포함될 수 있습니다.

.NET에서 Native AOT 애플리케이션을 개발하는 방법

.NET 8은 Native AOT라고도 알려진 사전 컴파일에 대한 포괄적인 지원을 제공합니다. 이러한 애플리케이션은 일반적으로 관리형 솔루션보다 더 빠르고 메모리를 덜 소비합니다.

.NET AOT 게시는 잘린 자체 포함 애플리케이션을 생성합니다. 이러한 애플리케이션:

  1. .NET 런타임이 설치되지 않은 컴퓨터에서 실행할 수 있습니다.
  2. Windows x64와 같은 특정 런타임 환경을 대상으로 합니다.
  3. 사용하지 않는 코드를 포함하지 마십시오.

.NET AOT 애플리케이션 배포

PDF 처리를 위한 Docotic.Pdf 라이브러리를 개발합니다. AOT 호환성은 2024년에 가장 인기 있는 기능 요청 중 하나입니다. 이 문서에서는 AOT 호환성을 달성하기 위한 여정을 설명합니다. .NET 프로젝트에서 AOT 문제를 찾아 수정하는 방법을 알아봅니다.

.NET AOT 게시 기본 사항

Native AOT 애플리케이션을 게시하려면 프로젝트 파일에 다음 속성을 추가합니다.

<PropertyGroup>
    <PublishAot>true</PublishAot>
</PropertyGroup>

일부 전제 조건을 설치해야 할 수도 있습니다. 그런 다음 Visual Studio에서 앱을 게시할 수 있습니다. 또는 dotnet publish 명령을 사용하여 명령줄에서 프로젝트를 게시할 수 있습니다.

dotnet publish -r linux-x64 -c Release

.NET 라이브러리에 대한 AOT 호환성

Native AOT 게시는 .NET 7 및 .NET 8에서 사용할 수 있습니다. .NET 7에서는 콘솔 애플리케이션만 게시할 수 있습니다. .NET 8에서는 ASP.NET Core 애플리케이션을 추가로 게시할 수 있습니다.

그러나 Docotic.Pdf 핵심 라이브러리는 .NET Standard 2.0을 대상으로 합니다. AOT 친화적으로 만들 수 있을까요? 물론.

.NET Standard, .NET 5 또는 .NET 6을 대상으로 하는 .NET 라이브러리에서 AOT 호환성 문제를 찾아 수정할 수 있습니다. 그러면 Native AOT 애플리케이션이 라이브러리에 의존할 수 있습니다.

AOT 문제 찾기

AOT 호환성에 대한 첫 번째 지원 요청은 다음과 같습니다.

우리는 WebAssembly로 컴파일된 Docotic.Pdf 사용을 고려하고 있습니다. AOT로 컴파일하면 트림 경고가 생성됩니다.
경고 IL2104: 'BitMiracle.Docotic.Pdf' 어셈블리에서 트림 경고가 생성되었습니다. 자세한 내용은 https://aka.ms/dotnet-illink/libraries를 참조하세요.

그다지 유익하지 않습니다. 문제를 재현하고 트림 경고에 대한 자세한 내용을 살펴보겠습니다.

게시 경고 받기

테스트 애플리케이션을 게시하는 것은 .NET AOT 문제를 찾는 가장 유연한 방법입니다. 다음을 수행해야 합니다.

  1. PublishAot = true를 사용하여 .NET 8 콘솔 프로젝트를 만듭니다.
  2. 프로젝트에 대한 참조를 추가하고 해당 유형 중 일부를 사용하십시오.

Docotic.Pdf에 사용하는 csproj 구성은 다음과 같습니다.

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <PublishAot>true</PublishAot>
        <TrimmerSingleWarn>false</TrimmerSingleWarn>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
        <WarningsAsErrors />
    </PropertyGroup>

    <ItemGroup>
        <ProjectReference Include="..\Docotic\Docotic.csproj" />
    </ItemGroup>
</Project>

TrimmerSingleWarn = false를 사용하면 경고에 대한 자세한 정보를 얻을 수 있습니다. 그렇지 않으면 "어셈블리 'X' 생성 트림 경고" 메시지만 표시됩니다.

다음은 Program.cs의 C# 코드입니다.

using BitMiracle.Docotic.Pdf;

using var pdf = new PdfDocument();

검사 중인 어셈블리를 강제로 로드하는 코드를 추가합니다. 게시 경고를 받기 위해 프로젝트의 모든 기능을 다룰 필요는 없습니다. 단일 유형을 사용하는 것으로 충분합니다. 하지만 런타임 오류 잡기에는 더 많은 코드를 작성해야 합니다.

이 프로젝트의 dotnet publish -r win-x64 -c Release 명령은 트림 및 AOT 경고를 반환했습니다. 목록의 짧은 버전은 다음과 같습니다.

AOT 분석 경고 IL3050: Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): 'RequiresDynamicCodeAttribute'가 있는 'System.Enum.GetValues(Type)' 멤버를 사용하면 AOT 컴파일 시 기능이 중단될 수 있습니다.

트림 분석 경고 IL2026: LogProviders.Log4NetLogProvider.Log4NetLogger.GetCreateLoggingEvent(ParameterExpression,UnaryExpression,ParameterExpression,UnaryExpression,Type): 'RequiresUnreferencedCodeAttribute'가 있는 'System.Linq.Expressions.Expression.Property(Expression,String)' 멤버를 사용하면 애플리케이션 코드를 트리밍할 때 기능이 중단될 수 있습니다.

트림 분석 경고 IL2057: LogProviders.LogProviderBase.FindType(String,String[]): 'System.Type.GetType(String)' 메서드의 'typeName' 매개 변수에 인식할 수 없는 값이 전달되었습니다.

트림 분석 경고 IL2070: LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String): 'this' 인수는 'System.Type.GetProperty(String)' 호출에서 'DynamicallyAccessedMemberTypes.PublicProperties'를 충족하지 않습니다. 'LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)' 메서드의 'loggerType' 매개변수에 일치하는 주석이 없습니다. 소스 값은 할당된 대상 위치에 선언된 요구 사항과 최소한 동일한 요구 사항을 선언해야 합니다.

트림 분석 경고 IL2075: LogProviders.Log4NetLogProvider.GetOpenNdcMethod(): 'this' 인수는 'System.Type.GetProperty(String)' 호출에서 'DynamicallyAccessedMemberTypes.PublicProperties'를 충족하지 않습니다. 'LogProviders.LogProviderBase.FindType(String,String)' 메서드의 반환 값에 일치하는 주석이 없습니다. 소스 값은 할당된 대상 위치에 선언된 요구 사항과 최소한 동일한 요구 사항을 선언해야 합니다.

근본 원인을 찾았습니다. 문제를 해결하기 전에 AOT 호환성 문제를 찾기 위한 대체 옵션을 확인해 보겠습니다.

AOT 호환성 분석기 사용

Microsoft는 .NET 7 또는 .NET 8 프로젝트에서 AOT 문제를 찾기 위해 Roslyn 분석기를 제공합니다. 코드를 작성할 때 AOT 문제를 강조할 수 있습니다. 그러나 테스트 애플리케이션을 게시하면 더 많은 문제가 감지됩니다.

정적 분석을 사용하려면 .csproj 파일에 다음 속성을 추가합니다.

<PropertyGroup>
    <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

.NET Standard 2.0 프로젝트에서 이러한 분석을 사용하려면 .NET 7 또는 .NET 8 프레임워크를 대상으로 해야 합니다. 프로젝트 구성은 다음과 같습니다.

<PropertyGroup>
    <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
    <IsAotCompatible Condition="'$(TargetFramework)'=='net8.0'">true</IsAotCompatible>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsAsErrors />
</PropertyGroup>

위 구성을 대규모 핵심 프로젝트에 적용했는데 1036개의 빌드 오류가 발생했습니다.

빌드 오류가 많음

그것들은 모두 AOT 호환성과 관련이 있습니까? IsAotCompatible 속성 없이 프로젝트를 다시 빌드했습니다. 그래도 1036개의 오류가 발생했습니다.

.NET Standard 2.0과 비교한 .NET 8의 개선 사항과 관련된 모든 오류입니다. 우리의 경우 대부분의 문제는 nullable 참조 유형에 대한 더 나은 지원에 관한 것이었습니다. 새로운 ArgumentOutOfRangeException.ThrowIfLessThanArgumentNullException.ThrowIfNull 도우미 메서드에 대한 CA1500 및 CA1512 오류도 많이 있었습니다.

모든 오류를 수정하고 IsAotCompatible 속성을 켰습니다. AOT 오류 없이 프로젝트 빌드가 완료되었습니다. 정적 분석에서는 게시 과정에서 발견된 경고를 하나도 포착하지 못했습니다 🙁

확인을 위해 코드 베이스에 다음 테스트 코드를 추가했습니다.

Type t = typeof(int);
t = typeof(List<>).MakeGenericType(t);
Console.WriteLine(Activator.CreateInstance(t));

string s = Console.ReadLine() ?? string.Empty;
Type? type = Type.GetType(s);
if (type != null)
{
    foreach (var m in type.GetMethods())
        Console.WriteLine(m.Name);
}

이 C# 코드는 .NET AOT 및 트리밍과 100% 호환되지 않습니다. 그리고 정적 분석을 통해 다음과 같은 오류가 확인되었습니다.

오류 IL3050: 'RequiresDynamicCodeAttribute'가 있는 'System.Type.MakeGenericType(params Type[])' 멤버를 사용하면 AOT 컴파일 시 기능이 중단될 수 있습니다. 이 인스턴스화에 대한 [네이티브] 코드는 런타임에 사용하지 못할 수도 있습니다.

오류 IL2057: 'System.Type.GetType(String)' 메서드의 'typeName' 매개 변수에 인식할 수 없는 값이 전달되었습니다. 대상 유형의 가용성을 보장하는 것은 불가능합니다.

따라서 Roslyn 분석기는 작동하며 몇 가지 문제를 찾을 수 있습니다. 우리의 경우에는 아무것도 찾지 못했습니다.

런타임 오류 잡기

Native AOT와 호환되는 라이브러리를 만드는 방법 문서에는 다음 원칙이 명시되어 있습니다.

애플리케이션이 AOT용으로 게시될 때 경고가 없으면 AOT 없이 AOT 이후와 동일하게 작동합니다.

실제로 AOT 이후 버전은 여전히 다르게 작동할 수 있습니다. 모든 AOT 및 트림 경고를 수정했습니다. 모든 자동 테스트를 통과했습니다. 테스트 응용 프로그램이 PDF 문서를 올바르게 처리했습니다. 그리고 동일한 애플리케이션의 AOT 버전에서 잘못된 문서가 생성되었습니다.

AOT 애플리케이션이 여전히 작동하지 않습니다

AOT 게시가 실수로 일부 필수 코드를 제거했습니다. 최적화로 필요한 코드 제거 문제가 발생했습니다. .NET 팀은 이를 신속하게 확인하고 수정했습니다. 그리고 수정 사항이 제공될 때까지 C# 코드에 해결 방법을 추가했습니다.

이 이야기의 교훈은 게시 및 정적 분석 경고만으로는 충분하지 않을 수 있다는 것입니다. 테스트 애플리케이션에서 프로젝트의 주요 기능을 사용하세요. 배포 후 테스트 애플리케이션을 실행합니다.

Docotic.Pdf 테스트 애플리케이션은 PDF 압축, 텍스트 추출 및 기타 주요 기능을 사용합니다. 우리는 이것을 다음과 같이 사용합니다:

AotCompatibility.exe compress "C:\compressed.pdf" "C:\input.pdf" "optional-password"

게시된 애플리케이션을 수동으로 테스트하는 것은 편리하지 않습니다. 프로세스를 자동화하는 것이 좋습니다.

AOT 문제를 자동으로 감지

Docotic.Pdf 개발은 자동 테스트를 기반으로 합니다. 모든 기능이나 버그에 대한 테스트가 있습니다. .NET AOT 호환성도 예외는 아닙니다.

Native AOT 애플리케이션 테스트를 자동화하는 방법에는 두 가지가 있습니다.

AOT 애플리케이션 외부에 테스트 배치

여기서는 일반 NUnit/xUnit/MSTest 프로젝트를 사용합니다. 이 프로젝트를 빌드하기 전에 AOT 테스트 애플리케이션을 게시합니다. 테스트에서는 Process.Start 메서드를 사용하여 게시된 앱을 실행합니다.

가장 큰 장점은 AOT 앱에서 테스트 인프라가 분리된다는 것입니다. 단위 테스트 프레임워크의 모든 기능을 사용할 수 있습니다. 테스트 프레임워크의 AOT 호환성 문제는 중요하지 않습니다. 기존 테스트 케이스를 재사용하는 것도 더 쉽습니다.

테스트 프로젝트의 샘플 구성은 GitHub에서 확인할 수 있습니다. Docotic.Tests 프로젝트에는 관리형 버전과 AOT 버전 모두에 대한 자동 테스트가 포함되어 있습니다. AotCompatibility는 AOT 게시를 위한 테스트 애플리케이션입니다.

AotCompatibility.csprojRelease 구성의 빌드 후 이벤트에 테스트 애플리케이션을 게시합니다. Visual Studio에서 "게시" 마법사를 사용할 때 '$(PublishProtocol)'=='' 조건은 이 단계를 비활성화합니다.

<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Release' And '$(PublishProtocol)'==''">
    <Exec Command="&quot;$(SolutionDir)Scripts\publish.bat&quot;" />
</Target>

publish.bat 스크립트는 간단합니다:

@echo off

SET AOT_TEST_DIR=%~dp0\..\AotCompatibility
dotnet publish "%TRIM_TEST_DIR%\AotCompatibility.csproj" --no-build /warnaserror /p:PublishProfile="%AOT_TEST_DIR%\Properties\PublishProfiles\win-x64-installer.pubxml"

여기서는 --no-build 인수를 사용합니다. 프로젝트는 빌드 후 이벤트 이전에 이미 빌드되었습니다. 우리는 그것을 다시 만들 필요가 없습니다.

NativeAot.cs의 테스트는 게시된 AotCompatibility.exe를 다른 인수로 실행합니다. Docotic.TestsAotCompatibility 프로젝트는 동일한 테스트 코드를 공유합니다. 이를 통해 코드를 복제하지 않고도 관리 버전과 AOT 버전을 확인할 수 있습니다.

AOT 애플리케이션 내부에 테스트 배치

또 다른 접근 방식은 AOT 테스트 애플리케이션 내에서 테스트를 작성하는 것입니다. 2024년 7월에는 MS Test 초기 미리보기만 사용할 수 있습니다. 자세한 내용은 Native AOT 애플리케이션 테스트 문서를 읽어보세요.

핵심 문제는 테스트 프레임워크의 AOT 문제로 인해 테스트를 신뢰할 수 없게 된다는 것입니다. 또한 제한된 테스트 프레임워크 기능 세트만 사용할 수 있습니다. 이것이 우리가 AOT 애플리케이션 외부에 테스트 인프라를 배치하는 것을 선호하는 이유입니다.

AOT 및 트림 경고 수정

게시 경고를 억제하지 마세요. 일반적으로 억제하는 것은 잘못된 해결책입니다. AOT를 억제하거나 경고를 자르면 프로젝트가 AOT와 호환된다는 의미입니다. 그러나 경고의 근본 원인은 여전히 존재합니다. 표시되지 않은 경고는 결국 애플리케이션의 런타임 오류로 이어질 수 있습니다.

각 경고와 관련된 코드를 제거하거나 다시 작성해 보십시오. 타사 구성요소를 업데이트하는 것도 도움이 될 수 있습니다.

Docotic.Pdf에서 AOT 및 트림 경고를 수정한 방법을 검토해 보겠습니다. 게시 경고 받기 섹션에서 그 중 일부를 이미 확인하셨습니다.

LibLog의 경고 자르기

로깅을 위해 LibLog 라이브러리를 사용했습니다. LibLog는 리플렉션을 사용하여 널리 사용되는 로깅 프레임워크를 감지하고 사용합니다. 설계상 AOT와 호환되지 않습니다.

현재 Microsoft.Extensions.Logging 패키지는 .NET 로그인의 표준입니다. 그리고 LibLog 개발은 이제 중단되었습니다.

이를 고려하여 우리는 코드에서 LibLog를 완전히 제거했습니다. 대신에 Docotic.Pdf.Logging 애드온을 출시했습니다. Microsoft.Extensions.Logging.Abstractions 인터페이스에 따라 다릅니다. 이는 핵심 라이브러리 크기를 줄이고 관련된 모든 트림 경고를 수정했습니다.

트림 경고 없음

Docotic.Layout 애드온의 디버깅 코드

Docotic.Layout 애드온에서는 내부 디버깅을 위해 리플렉션을 사용합니다. 이로 인해 IL2075 경고가 발생했습니다.

해당 코드는 코드베이스에 존재하지만 클라이언트는 공개 API를 통해 이를 사용할 수 없습니다. 해결 방법은 Release 구성에서 코드를 제외하는 것입니다. 이제 Debug 구성에서만 사용됩니다.

BouncyCastle의 AOT 경고 IL3050

BouncyCastle 라이브러리는 PDF 문서에 서명하는 데 도움이 됩니다. 다음 경고는 BouncyCastle 코드에서 발생합니다.

Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): 'RequiresDynamicCodeAttribute'가 있는 'System.Enum.GetValues(Type)' 멤버를 사용하면 AOT 컴파일 시 기능이 중단될 수 있습니다. 런타임에 열거형 유형의 배열을 생성하는 것이 불가능할 수도 있습니다. 대신 GetValues<TEnum> 오버로드 또는 GetValuesAsUnderlyingType 메서드를 사용하세요.

BouncyCastle은 AOT와 호환되지 않는 Enum.GetValues(Type) 메서드를 사용합니다.

가장 간단한 해결책은 Enum.GetValues<T>() 오버로드를 대신 사용하는 것입니다. 최신 BouncyCastle 버전에서는 이미 이를 사용하고 있습니다. 안타깝게도 이 오버로드는 .NET 5 이상에서만 사용할 수 있습니다. 이는 .NET Standard 2.0의 옵션이 아닙니다.

우리는 BouncyCastle 코드를 더 깊이 파고 분석했습니다. BouncyCastle은 열거형 상수의 난독화를 방지하기 위해 이를 사용하는 것으로 나타났습니다. 대신 [System.Reflection.Obfuscation(Exclude = true)] 속성을 사용하여 해당 열거형에 대한 난독화를 비활성화했습니다. 그리고 더 이상 필요하지 않은 Enum.GetValues(Type) 사용법을 제거했습니다.

난독화된 빌드의 이상한 문제

우리는 프로덕션 빌드를 보호하기 위해 난독화를 사용합니다. 놀랍게도 난독화된 빌드에 대한 .NET AOT 배포에서는 유사한 경고가 반환되었습니다.

<알 수 없음>:0: 오류: '__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon' 기호가 이미 정의되어 있습니다.

이러한 오류에는 연관된 IL30## 또는 IL2### 코드가 없습니다. 관련 코드를 식별하는 방법이 불분명했습니다.

난독화된 버전에서만 오류가 발생했습니다. 난독 처리기의 버그 일 수 있습니다. 난독처리기를 업데이트했지만 오류가 여전히 존재했습니다.

문제의 범위를 좁힐 필요가 있었습니다. 우리는 이진 검색을 기반으로 다음 프로세스를 사용했습니다.

  1. 네임스페이스의 절반에 대해 난독화를 비활성화합니다. 오류가 발생하는지 확인하세요. 오류를 일으키는 네임스페이스를 찾을 때까지 계속합니다.
  2. 1단계에서 네임스페이스 유형의 절반에 대해 난독화를 비활성화합니다. 오류가 발생하는지 확인하세요. 오류를 일으키는 유형을 찾을 때까지 계속하십시오.
  3. 2단계에서 입력한 유형의 멤버 절반에 대해 난독화를 비활성화합니다. 오류가 발생하는지 확인하세요. 오류를 발생시키는 구성원을 찾을 때까지 계속합니다.
  4. 3단계에서 회원의 코드를 검토합니다. 특이한 부분을 주석 처리하고 오류가 발생하는지 확인합니다.

우리는 1단계에서 끝났습니다 🙂 모든 네임스페이스에 대한 난독화를 비활성화한 후에도 오류가 계속 발생했습니다.

생성된 어셈블리를 ILSpy로 분석했습니다. 예상치 못한 컴파일러 생성 유형이 몇 가지 발견되었습니다. ILSpy는 이러한 C# 코드에서 이러한 유형의 사용법을 보여주었습니다.

interface X
{
    void f();
}

X obj = ..;
if (obj.f != null)
    ..

이 이상한 코드는 이전 스타일 C 코드를 C#으로 마이그레이션에서 나왔습니다. if (obj.f != null) 조건은 완전히 중복됩니다. 이러한 조건을 제거하고 오류가 사라졌습니다.

GetCallingAssembly를 사용하지 마세요

또 다른 런타임 문제가 발견되었습니다. 다음 오류로 인해 AOT 게시 후 LicenseManager.AddLicenseData(string) 메서드 호출이 실패했습니다.

Assembly.GetCallingAssembly()에서 System.PlatformNotSupportedException이 발생합니다. 이 플랫폼에서는 작업이 지원되지 않습니다.

.NET 9의 Assembly.GetCallingAssembly 메서드Native AOT에 대해 구현되어야 함. 수정 사항이 준비될 때까지 부정적인 영향을 최소화하기 위해 코드를 리팩터링했습니다. 이제 Application 라이센스의 유효성을 검사하기 위해서만 호출 어셈블리 속성을 사용합니다. 다른 라이센스 유형은 AOT와 호환됩니다.

특수 속성으로 코드 표시

다행히 핵심 Docotic.Pdf 라이브러리와 대부분의 추가 기능에서 모든 AOT 및 트림 경고를 수정할 수 있었습니다. 그러나 모든 AOT 호환되지 않는 코드를 다시 작성하지 못할 수도 있습니다.

.NET은 이러한 상황에 대한 특별한 특성을 제공합니다. 알려진 AOT 문제에 대해 클라이언트에 알리기 위해 API에 속성을 표시합니다. API 사용자는 표시된 메소드를 호출할 때 경고를 받게 됩니다.

AOT 호환되지 않는 .NET 코드를 표시하기 위한 주요 속성은 다음과 같습니다.

  • RequiresDynamicCode
  • RequiresUnreferencedCode
  • DynamicallyAccessedMembers

공식 AOT 경고 소개트리밍을 위한 .NET 라이브러리 준비 기사에서 이러한 특성에 대한 자세한 내용을 확인할 수 있습니다.

결론

Native AOT 배포는 .NET 세계에서 큰 진전입니다. 일반 C# 코드를 작성하고 네이티브 애플리케이션이나 라이브러리를 얻을 수 있습니다. 이러한 응용 프로그램은 일반적으로 더 빠르며 .NET 런타임을 설치하지 않고도 실행할 수 있습니다. C, Rust 또는 기타 비.NET 프로그래밍 언어에서 게시된 DLL을 사용할 수도 있습니다.

AOT 호환성 문제를 찾으려면 테스트 .NET 8 애플리케이션을 게시하세요. Roslyn 분석기도 있지만 더 적은 수의 문제를 포착합니다.

핵심 Docotic.Pdf 라이브러리는 이제 AOT 및 트리밍과 호환됩니다. 다음 추가 기능도 호환됩니다.

  • Layout 애드온
  • Gdi 애드온
  • Logging 애드온

HTML-PDF 추가 기능에는 여전히 트림 경고가 포함되어 있습니다. 이것이 Native AOT 로드맵의 다음 목표입니다.

Native AOT 또는 PDF 처리에 대해 자유롭게 질문해 주세요.