AvaloniaUI NativeAot 시 기본적인 실수

AvaloniaUI는 NativeAot를 지원합니다. 그렇기 때문에 저를 포함한 많은 분들이 실수하실 것 같아서 남깁니다. (저는 이미 했고)

NativeAot는 게시 후 .NET 프로젝트가 .NET과 무관해지는 기술입니다. 그렇기 때문에 더이상 .NET Assembly 라고 될지도 모르겠네요.

일반적으로 NativeAot 게시를 선택하면 csproj에 PublishAot 속성이 활성화됩니다.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
    <ApplicationManifest>app.manifest</ApplicationManifest>
    <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
    <PublishAot>true</PublishAot>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Avalonia" Version="11.3.6" />
    <PackageReference Include="Avalonia.Desktop" Version="11.3.6" />
    <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.6" />
    <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.6" />
    <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
    <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.6" />
  </ItemGroup>
</Project>

그런데 이렇게 하면 VS2022나 Jetbrains Rider로 axaml 파일을 열었을 때 오류가 발생합니다. 빌드해서 돌리면 잘 도는데 말이죠.

dynamic code generation is not supported on this platform

그래서 당연하게도 아래와 같이 Debug일때는 NativeAot를 비활성하면 됩니다. (pdb 파일 포함은 NativeAot 배포에는 적용 안되네요.)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
    <ApplicationManifest>app.manifest</ApplicationManifest>
    <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
    <PublishAot>false</PublishAot>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <DebugType>embedded</DebugType>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
    <DebugType>embedded</DebugType>
    <PublishAot>true</PublishAot>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Avalonia" Version="11.3.6" />
    <PackageReference Include="Avalonia.Desktop" Version="11.3.6" />
    <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.6" />
    <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.6" />
    <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
    <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.6" />
  </ItemGroup>
</Project>

게시 프로필

<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
  <PropertyGroup>
    <Configuration>Release</Configuration>
    <Platform>Any CPU</Platform>
    <PublishDir>bin\Release\net8.0\publish\win-x64\</PublishDir>
    <PublishProtocol>FileSystem</PublishProtocol>
    <_TargetId>Folder</_TargetId>
    <TargetFramework>net8.0</TargetFramework>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <SelfContained>false</SelfContained>
    <PublishSingleFile>false</PublishSingleFile>
    <PublishReadyToRun>false</PublishReadyToRun>
  </PropertyGroup>
</Project>

Publish 결과

4개의 좋아요

WPF나 Windows Forms와 달리 아발로니아 디자인 타임은 코드 기반으로 렌더링하지 않고 빌드 산출물을 이용해서 디자인 타임을 구현하는 것 같습니다.

그래서 일반적인 빌드 환경에서는 AOT를 사용하지 않고, Publish 절차를 따로 두어서 그 때에만 NativeAOT 속성을 켜서 빌드하도록 컴파일 옵션을 명령줄 수준에서 변경하는 것을 채택하는 것이 개발하기에는 편한 것 같습니다.

그리고 더 깊게 들어가면, Program.cs 파일 내 Program 클래스에서 아래 메서드를 리플렉션으로 찾는 시도를 하는데 참조하는 과정에서 문제가 생기는 것 같습니다. 이것이 말씀하신 문제가 발생하는 원인으로 보입니다. (참고로 BuildAvaloniaApp 메서드의 리턴 타입과 파라미터 인자 목록, static 여부가 중요하고, public이든 private이든 찾는데는 문제가 없습니다.)

        // Avalonia configuration, don't remove; also used by visual designer.
        public static AppBuilder BuildAvaloniaApp()
        {
            return AppBuilder.Configure<App>()
                        .UsePlatformDetect()
                        .LogToTrace();
        }

여담이지만, Avalonia를 Generic Host에 통합할 수 있는 방법을 찾기 위해서 코드를 테스트해보다가, 디자인 타임 프리뷰도 그대로 유지하고 싶어서 아래처럼 nudge를 적용하기도 했습니다. (식탁보 vNext에서 발췌)

	[STAThread]
	[SupportedOSPlatform("windows")]
	private static void Main(string[] args)
	{
		var builder = WebApplication.CreateBuilder(args);

		builder.Services.AddAvaloniauiDesktopApplication<App>(BuildAvaloniaApp);

		using var app = builder.Build();

        app.RunAvaloniauiApplication(args).GetAwaiter().GetResult();
	}

	// This method is used by both AppHost Avalonia runtime and the Avalonia Designer.
	private static AppBuilder BuildAvaloniaApp(AppBuilder? app)
	{
		if (app == null)
			app = AppBuilder.Configure<App>();

		return app
			.UsePlatformDetect()
			.LogToTrace();
	}

	// This method is required for use with the Avalonia designer.
	// The Avalonia designer will look for this method regardless of whether or not it is private.
	private static AppBuilder BuildAvaloniaApp()
		=> BuildAvaloniaApp(null);
3개의 좋아요

혹시 이 부분이 어느 메서드를 파고들어가면 나오는지 짚어 주실 수 있을까요?
AvaloniaUI 코드를 Clone해서 System.Reflection 으로 검색하니까 엄청나게 많이 나오네요…안쓰이는 네임스페이드도 함께 포함해서…;;

1개의 좋아요

말씀해주신 내용이 저도 궁금해서 찾아봤는데, 역시 코드베이스가 방대하다보니 Github Copilot으로도 몇 번의 문답이 필요한 것 같습니다. 아래는 GPT-5 Github Copilot 으로 정리한 내용입니다.

일단 디자이너가 사용할 화면을 불러오는 과정에서 쓰이는 코드는 아래 위치에 있는 것 같습니다.

다만 이것만으로 전체 동작이 설명되는 것은 아니고, 디자인 타임 호스트 프로세스, 익스텐션, IDE 사이를 프로세스 공간을 섞지 않기 위해서 굉장히 많은 계층과 그 사이의 통신을 BSON과 TCP 소켓 등을 사용해서 처리하고 있었습니다. (앞의 답글에서 리플렉션을 사용했다는 말에는 취소선을 그었습니다.)


다음은 “리플렉션만 쓰는 게 아니라 BSON 소켓 통신을 사용해 디자인-타임 미리보기(프리뷰)를 구현하는” 전체 아키텍처 설명입니다.

개요

  • IDE 확장(Visual Studio/VS Code)과 프리뷰어 호스트 프로세스(Avalonia.Designer.HostApp)가 별도 프로세스로 분리됩니다.

  • 통신은 로컬 루프백(127.0.0.1)에서 TCP 소켓 위에 BSON 직렬화를 사용하는 프로토콜로 이뤄집니다.

  • 사용자의 앱 초기화(BuildAvaloniaApp 호출)와 XAML 로드/렌더링은 호스트 프로세스에서 수행됩니다. IDE 프로세스는 호스트와 메시지를 주고받으며 비트맵 프레임과 오류만 수신/표시합니다.

구성 요소와 역할

  • IDE 확장 쪽

    • 미리보기용 서버 소켓을 Listen합니다.

    • 호스트 프로세스를 실행(dotnet exec … Avalonia.Designer.HostApp.dll …)하고, 호스트가 IDE 서버에 접속하도록 합니다.

    • XAML 텍스트/뷰포트/렌더 정보 등을 메시지로 전송하고, 렌더링 결과 프레임·에러를 수신해 디자이너에 그립니다.

    • 관련 코드:

  • 프리뷰어 호스트 프로세스 쪽

    • IDE가 띄운 서버에 tcp-bson으로 Connect하고, 디자인 세션을 수립한 뒤 요청을 처리합니다.

    • 사용자의 앱 어셈블리를 로드하고 BuildAvaloniaApp을 리플렉션으로 찾아 AppBuilder를 구성한 뒤, 디자인-타임 플랫폼으로 초기화합니다.

    • 관련 코드:

전송 계층과 프로토콜

  • 전송 계층

    • BsonTcpTransport은 TCP 스트림 위에 BSON 직렬화로 메시지를 송수신합니다.

    • IDE: Listen(Loopback, port, onAccept)

    • HostApp: Connect(Loopback, port)

  • 주요 메시지 흐름(예)

    1. HostApp → IDE: StartDesignerSessionMessage(세션 시작 알림)

    2. IDE → HostApp: ClientSupportedPixelFormatsMessage, ClientRenderInfoMessage, ClientViewportAllocatedMessage(렌더링 환경 제시)

    3. IDE → HostApp: UpdateXamlMessage(XAML 텍스트, 또는 AssemblyPath+Xaml 경로)

    4. HostApp → IDE: UpdateXamlResultMessage(에러 또는 성공·핸들/프레임 정보)

    5. 이후 Viewport 크기/스케일 업데이트, 입력 이벤트 전달, 프레임(비트맵) 갱신 루프

동작 순서(요약)

  1. IDE 확장이 BsonTcpTransport.Listen으로 로컬 서버를 엽니다.

  2. IDE가 호스트 프로세스(Avalonia.Designer.HostApp)를 실행하며, --transport tcp-bson://127.0.0.1:{port}/ 인자를 전달합니다.

  3. HostApp이 해당 포트로 Connect하고, RemoteDesignerEntryPoint.Main에서 사용자 앱 어셈블리를 로드 후 AppBuilder.Configure(entryPointType)을 통해 BuildAvaloniaApp을 리플렉션으로 호출합니다.

  4. 디자인-타임 플랫폼(PreviewerWindowingPlatform 등)으로 초기화하여 XAML 업데이트, 렌더링, 뷰포트/입력 이벤트를 메시지로 주고받습니다.

왜 소켓 + BSON 아키텍처인가

  • 안정성: 사용자의 앱 로드/렌더링을 IDE 프로세스와 분리해 IDE의 안정성을 높입니다. 호스트가 크래시해도 IDE는 살아있고 세션만 재시작하면 됩니다.

  • 호환성: .NET Framework/ .NET(Core/5+/6+/8+) 혼용 시에도 “dotnet exec … HostApp.dll” 식으로 적절한 런타임 경계 내에서 실행 가능.

  • 성능/유연성: 프레임 데이터를 바이너리(BSON)로 효율적으로 전달하고, 다양한 메시지(에러, 뷰포트, 입력 등)를 확장 가능하게 교환.

  • 보안/격리: 로컬 루프백 포트만 사용하여 외부 노출을 최소화하고, 사용자 코드/의존성을 IDE에서 직접 로드하지 않습니다.

추가 참고

4개의 좋아요