Esta página puede contener texto traducido automáticamente.

Cómo desarrollar aplicaciones Native AOT en .NET

.NET 8 incorpora compatibilidad completa con la compilación ahead-of-time, también conocida como Native AOT. Estas aplicaciones suelen iniciarse más rápido y consumir menos memoria que las soluciones administradas.

La publicación .NET AOT genera aplicaciones autocontenidas con recorte. Estas aplicaciones:

  1. Pueden ejecutarse en una máquina que no tiene instalado el tiempo de ejecución de .NET.
  2. Se dirigen a un entorno de ejecución específico, como Windows x64.
  3. No contienen código no utilizado.

Desplegar aplicaciones .NET AOT

Desarrollamos la biblioteca Docotic.Pdf para el procesamiento de PDF. La compatibilidad con AOT es una de las solicitudes de características más populares en 2024. Este artículo describe nuestro recorrido hacia la compatibilidad con AOT. Aprenderá a encontrar y corregir problemas de AOT en proyectos .NET.

Conceptos básicos de la publicación .NET AOT

Para publicar una aplicación Native AOT, agregue la siguiente propiedad a su archivo de proyecto:

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

Es posible que también necesites instalar algunos prerrequisitos. Luego, puedes publicar tu aplicación desde Visual Studio. O puedes publicar el proyecto desde la línea de comandos usando el comando dotnet publish:

dotnet publish -r linux-x64 -c Release

Compatibilidad con AOT para bibliotecas .NET

La publicación Native AOT está disponible en .NET 7 y .NET 8. En .NET 7, solo puedes publicar aplicaciones de consola. En .NET 8, además puedes publicar aplicaciones ASP.NET Core.

Pero la biblioteca principal Docotic.Pdf tiene como destino .NET Standard 2.0. ¿Podemos hacerla compatible con AOT? Por supuesto.

Aun así, puedes encontrar y corregir problemas de compatibilidad con AOT en bibliotecas .NET dirigidas a .NET Standard, .NET 5 o .NET 6. Entonces, las aplicaciones Native AOT pueden depender de la biblioteca.

Encontrar problemas de AOT

La primera solicitud de soporte sobre compatibilidad con AOT suena así:

Estamos considerando usar Docotic.Pdf compilado a WebAssembly. Produce advertencias de recorte cuando lo compilamos a AOT:
warning IL2104: Assembly 'BitMiracle.Docotic.Pdf' produced trim warnings. For more information see https://aka.ms/dotnet-illink/libraries

No es muy informativa. Reproduzcamos el problema y obtengamos más detalles sobre estas advertencias de recorte.

Obtener advertencias de publicación

Publicar una aplicación de prueba es la forma más flexible de encontrar problemas de AOT de .NET. Necesita:

  1. Crear un proyecto de consola .NET 8 con PublishAot = true
  2. Agregar una referencia a su proyecto y usar algunos de sus tipos

Aquí está la configuración de csproj que usamos para Docotic.Pdf:

<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 le permite obtener información detallada sobre las advertencias. De lo contrario, solo recibirá el mensaje "Assembly 'X' produced trim warnings".

Y aquí está el código C# en Program.cs:

using BitMiracle.Docotic.Pdf;

using var pdf = new PdfDocument();

Agregue algo de código para forzar la carga del ensamblado que se está comprobando. No es necesario cubrir toda la funcionalidad de su proyecto para obtener advertencias de publicación. Usar un solo tipo es suficiente. Pero necesita escribir más código para capturar errores en tiempo de ejecución.

El comando dotnet publish -r win-x64 -c Release para este proyecto devolvió advertencias de recorte y de AOT. Aquí está la versión breve de la lista:

AOT analysis warning IL3050: Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): Using member 'System.Enum.GetValues(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling.

Trim analysis warning IL2026: LogProviders.Log4NetLogProvider.Log4NetLogger.GetCreateLoggingEvent(ParameterExpression,UnaryExpression,ParameterExpression,UnaryExpression,Type): Using member 'System.Linq.Expressions.Expression.Property(Expression,String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code.

Trim analysis warning IL2057: LogProviders.LogProviderBase.FindType(String,String[]): Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'.

Trim analysis warning IL2070: LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The parameter 'loggerType' of method 'LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Trim analysis warning IL2075: LogProviders.Log4NetLogProvider.GetOpenNdcMethod(): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The return value of method 'LogProviders.LogProviderBase.FindType(String,String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Encontramos la causa raíz. Antes de corregir los problemas, comprobemos opciones alternativas para encontrar problemas de compatibilidad con AOT.

Usar analizadores de compatibilidad con AOT

Microsoft proporciona analizadores de Roslyn para encontrar problemas de AOT en proyectos .NET 7 o .NET 8. Pueden resaltar problemas de AOT cuando escribe el código. Pero la publicación de una aplicación de prueba detecta más problemas.

Para usar el análisis estático, agregue la siguiente propiedad al archivo .csproj:

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

Debe dirigirse al framework .NET 7 o .NET 8 para usar este tipo de análisis en un proyecto .NET Standard 2.0. La configuración del proyecto puede verse así:

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

Aplicamos la configuración anterior a nuestro gran proyecto principal y obtuvimos 1036 errores de compilación.

Muchos errores de compilación

¿Estaban todos relacionados con la compatibilidad con AOT? Habíamos recompilado el proyecto sin la propiedad IsAotCompatible. Aun así, 1036 errores.

Todos los errores estaban relacionados con las mejoras en .NET 8 en comparación con .NET Standard 2.0. En nuestro caso, la mayoría de los problemas se debían al mejor soporte para tipos de referencia anulables. También había muchos errores CA1500 y CA1512 sobre los nuevos métodos auxiliares ArgumentOutOfRangeException.ThrowIfLessThan y ArgumentNullException.ThrowIfNull.

Corregimos todos los errores y activamos la propiedad IsAotCompatible. La compilación del proyecto finalizó sin errores de AOT. El análisis estático no detectó ninguna de las advertencias encontradas durante el proceso de publicación 🙁

Para confirmarlo, habíamos agregado el siguiente código de prueba a nuestra base de código:

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);
}

Este código C# es 100 % incompatible con .NET AOT y con el recorte. Y el análisis estático identificó los siguientes errores:

error IL3050: Using member 'System.Type.MakeGenericType(params Type[])' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. The native code for this instantiation might not be available at runtime.

error IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'. It's not possible to guarantee the availability of the target type.

Así que los analizadores de Roslyn funcionan y pueden encontrar algunos problemas. En nuestro caso, no encontraron nada.

Capturar errores en tiempo de ejecución

El artículo Cómo hacer compatibles las bibliotecas con Native AOT indica el siguiente principio:

Si una aplicación no tiene advertencias al publicarse para AOT, se comportará igual después de AOT que sin AOT.

En la práctica, la versión después de AOT aún puede comportarse de manera diferente. Habíamos corregido todas las advertencias de AOT y de recorte. Todas las pruebas automáticas pasaron. La aplicación de prueba procesó correctamente documentos PDF. Y la versión AOT de la misma aplicación generó documentos incorrectos.

La aplicación AOT seguía sin funcionar

La publicación AOT eliminó por error parte del código requerido. Creamos el problema Optimization removes necessary code. El equipo de .NET lo confirmó y lo corrigió rápidamente. Y agregamos una solución temporal a nuestro código C# hasta que la corrección esté disponible.

La moraleja de esta historia es que las advertencias de publicación y de análisis estático pueden no ser suficientes. Use las características clave de su proyecto en la aplicación de prueba. Y ejecute la aplicación de prueba después de la implementación.

La aplicación de prueba de Docotic.Pdf usa compresión de PDF, extracción de texto y otras funcionalidades clave. La usamos así:

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

Las pruebas manuales de la aplicación publicada no son cómodas. Vale la pena automatizar el proceso.

Detectar problemas de AOT automáticamente

El desarrollo de Docotic.Pdf se basa en pruebas automáticas. Hay pruebas para cada característica o error. La compatibilidad con .NET AOT no es una excepción.

Hay dos formas de automatizar las pruebas de aplicaciones Native AOT.

Colocar las pruebas fuera de la aplicación AOT

Aquí se usa un proyecto normal de NUnit/xUnit/MSTest. Antes de compilar este proyecto, publica la aplicación de prueba AOT. Las pruebas ejecutan la aplicación publicada usando el método Process.Start.

La principal ventaja es la separación de la infraestructura de pruebas de la aplicación AOT. Puede usar todas las capacidades de los marcos de pruebas unitarias. No importan los problemas de compatibilidad con AOT en el marco de pruebas. También es más fácil reutilizar casos de prueba existentes.

La configuración de ejemplo del proyecto de pruebas está disponible en GitHub. El proyecto Docotic.Tests contiene pruebas automáticas para las versiones administrada y AOT. AotCompatibility es una aplicación de prueba para la publicación AOT.

AotCompatibility.csproj publica la aplicación de prueba en un evento posterior a la compilación en la configuración Release. La condición '$(PublishProtocol)'=='' deshabilita este paso cuando usa el asistente "Publish" en Visual Studio.

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

El script publish.bat es simple:

@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"

Tenga en cuenta que aquí usamos el argumento --no-build. El proyecto ya está compilado antes del evento posterior a la compilación. No necesitamos compilarlo de nuevo.

Las pruebas en NativeAot.cs ejecutan AotCompatibility.exe publicado con diferentes argumentos. Los proyectos Docotic.Tests y AotCompatibility comparten el mismo código probado. Eso nos permite comprobar las versiones administrada y AOT sin duplicar código.

Colocar las pruebas dentro de la aplicación AOT

Un enfoque alternativo es escribir pruebas dentro de la aplicación de prueba AOT. En julio de 2024, solo puede usar la vista previa temprana de MS Test. Lea el artículo Pruebas de sus aplicaciones Native AOT para obtener más detalles.

El problema principal es que cualquier problema de AOT en el marco de pruebas hará que sus pruebas sean poco fiables. Y solo está disponible un conjunto limitado de características del marco de pruebas. Por eso preferimos colocar la infraestructura de pruebas fuera de la aplicación AOT.

Corregir advertencias de AOT y de recorte

Evite suprimir advertencias de publicación. Normalmente, suprimirlas es una solución incorrecta. Cuando suprime advertencias de AOT o de recorte, está afirmando que su proyecto es compatible con AOT. Sin embargo, la causa raíz de la advertencia sigue ahí. Cualquier advertencia suprimida puede acabar provocando un fallo en tiempo de ejecución de su aplicación.

Intente eliminar o reescribir el código asociado con cada advertencia. Actualizar componentes de terceros también puede ayudar.

Revisemos cómo corregimos las advertencias de AOT y de recorte en Docotic.Pdf. Ya vio algunas en la sección Obtener advertencias de publicación.

Advertencias de recorte de LibLog

Usábamos la biblioteca LibLog para el registro. LibLog depende de la reflexión para detectar y usar marcos de registro populares. Es incompatible con AOT por diseño.

Actualmente, el paquete Microsoft.Extensions.Logging es el estándar para el registro en .NET. Y el desarrollo de LibLog está congelado.

Dado eso, eliminamos por completo LibLog de nuestro código. En su lugar, publicamos el complemento Docotic.Pdf.Logging. Depende de las interfaces Microsoft.Extensions.Logging.Abstractions. Eso redujo el tamaño de la biblioteca principal y corrigió todas las advertencias de recorte relacionadas.

Sin advertencias de recorte

Código de depuración en el complemento Docotic.Layout

En el complemento de layout, usamos reflexión para depuración interna. Eso provocó la advertencia de análisis de recorte IL2075.

El código correspondiente existe en la base de código, pero los clientes no pueden usarlo mediante la API pública. La solución es excluir el código de la configuración Release. Ahora solo se usa en la configuración Debug.

Advertencia AOT IL3050 de BouncyCastle

La biblioteca BouncyCastle nos ayuda a firmar documentos PDF. La siguiente advertencia proviene de su código:

Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): Using member 'System.Enum.GetValues(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. It might not be possible to create an array of the enum type at runtime. Use the GetValues<TEnum> overload or the GetValuesAsUnderlyingType method instead.

BouncyCastle usa el método Enum.GetValues(Type), que no es compatible con AOT.

La solución más simple sería usar el sobrecargado Enum.GetValues<T>() en su lugar. La versión más reciente de BouncyCastle ya lo usa. Por desgracia, esta sobrecarga solo está disponible en .NET 5 o posterior. Eso no es una opción para .NET Standard 2.0.

Profundizamos más y analizamos el código de BouncyCastle. Resultó que BouncyCastle lo usa para evitar la ofuscación de constantes de enumeración. En su lugar, deshabilitamos la ofuscación para las enumeraciones correspondientes usando el atributo [System.Reflection.Obfuscation(Exclude = true)]. Y eliminamos los usos de Enum.GetValues(Type) que ya no eran necesarios.

Un problema extraño en la compilación ofuscada

Usamos ofuscación para proteger las compilaciones de producción. Sorprendentemente, la implementación AOT de .NET para la compilación ofuscada devolvió advertencias similares:

<unknown>:0: error: symbol '__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon' is already defined

Esos errores no tenían códigos asociados IL30## o IL2###. No estaba claro cómo identificar el código asociado.

Los errores ocurrieron solo en la versión ofuscada. Podría ser un error del ofuscador. Actualizamos el ofuscador, pero los errores seguían presentes.

Necesitábamos acotar el alcance del problema. Usamos el siguiente proceso, basado en una búsqueda binaria:

  1. Deshabilite la ofuscación para la mitad de los espacios de nombres. Compruebe si se producen errores. Continúe hasta encontrar los espacios de nombres que provocan los errores.
  2. Deshabilite la ofuscación para la mitad de los tipos en el espacio de nombres del paso uno. Compruebe si se producen errores. Continúe hasta encontrar los tipos que provocan los errores.
  3. Deshabilite la ofuscación para la mitad de los miembros en el tipo del paso dos. Compruebe si se producen errores. Continúe hasta encontrar los miembros que provocan los errores.
  4. Revise el código del miembro del paso tres. Comente las partes inusuales y compruebe si se producen errores.

Terminamos en el paso uno 🙂 Después de deshabilitar la ofuscación para todos los espacios de nombres, los errores seguían ocurriendo.

Analizamos el ensamblado generado con ILSpy. Encontramos algunos tipos generados por el compilador que no esperábamos. ILSpy mostró sus usos a partir de un código C# similar:

interface X
{
    void f();
}

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

Este código extraño provenía de la migración de código C antiguo a C#. La condición if (obj.f != null) es completamente redundante. Eliminamos esas condiciones y los errores desaparecieron.

No usar GetCallingAssembly

Detectamos otro problema en tiempo de ejecución. La llamada al método LicenseManager.AddLicenseData(string) falló después de la publicación AOT con el siguiente error:

Assembly.GetCallingAssembly() throws System.PlatformNotSupportedException: Operation is not supported on this platform.

El método Assembly.GetCallingAssembly debería implementarse para Native AOT en .NET 9. Hasta que la corrección esté lista, refactorizamos nuestro código para minimizar el impacto negativo. Ahora usamos los atributos del ensamblado llamador solo para validar la Licencia de aplicación. Los otros tipos de licencia son compatibles con AOT.

Marcar el código con atributos especiales

Afortunadamente, hemos podido corregir todas las advertencias de AOT y de recorte en la biblioteca principal Docotic.Pdf y en la mayoría de los complementos. Pero puede que no pueda reescribir todo el código incompatible con AOT.

.NET proporciona atributos especiales para estas situaciones. Se marca la API con atributos para informar a los clientes sobre problemas de AOT conocidos. Los usuarios de su API recibirán una advertencia al llamar al método marcado.

Los atributos clave para marcar código .NET incompatible con AOT son:

  • RequiresDynamicCode
  • RequiresUnreferencedCode
  • DynamicallyAccessedMembers

Puede encontrar más información sobre estos atributos en los artículos oficiales Introducción a las advertencias de AOT y Preparar bibliotecas .NET para el recorte.

Conclusión

La implementación Native AOT es un gran avance en el mundo de .NET. Puede escribir código C# normal y obtener una aplicación o biblioteca nativa. Este tipo de aplicaciones suele ser más rápido y puede ejecutarse sin que esté instalado el tiempo de ejecución de .NET. Incluso puede usar DLL publicadas desde C, Rust u otros lenguajes de programación que no sean .NET.

Publique una aplicación de prueba .NET 8 para encontrar problemas de compatibilidad con AOT. También hay analizadores de Roslyn, pero detectan menos problemas.

La biblioteca principal Docotic.Pdf ahora es compatible con AOT y con el recorte. Los siguientes complementos también son compatibles:

  • Complemento Conformance
  • Complemento Layout
  • Complemento Gdi
  • Complemento Logging

El complemento HTML to PDF todavía contiene advertencias de recorte. Ese es nuestro próximo objetivo en la hoja de ruta de Native AOT.

No dude en hacernos preguntas sobre Native AOT o el procesamiento de PDF.