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

Cómo desarrollar aplicaciones Native AOT en .NET

.NET 8 ofrece soporte integral para la compilación anticipada, también conocida como Native AOT. Estas aplicaciones suelen ser más rápidas y consumen menos memoria que las soluciones administradas.

La publicación .NET AOT produce aplicaciones recortadas e independientes. Tales aplicaciones:

  1. Puede ejecutarse en una máquina que no tenga instalado el tiempo de ejecución .NET.
  2. Apunte a un entorno de ejecución específico, como Windows x64.
  3. No contenga código no utilizado.

Implementar aplicaciones .NET AOT

Desarrollamos la biblioteca Docotic.Pdf para procesamiento de PDF. La compatibilidad con AOT es una de las solicitudes de funciones más populares en 2024. Este artículo describe nuestro viaje para lograr la compatibilidad con AOT. Aprenderá cómo encontrar y solucionar problemas de AOT en proyectos .NET.

Conceptos básicos de 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 requisitos previos. Luego, puede publicar su aplicación desde Visual Studio. O puede publicar el proyecto desde la línea de comando usando el comando dotnet publish: dotnet publish -r linux-x64 -c Release

Compatibilidad AOT para bibliotecas .NET

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

Pero la biblioteca principal Docotic.Pdf está dirigida a .NET Standard 2.0. ¿Podemos hacerlo compatible con AOT? Por supuesto.

Aún puede encontrar y solucionar problemas de compatibilidad de AOT en bibliotecas .NET dirigidas a .NET Standard, .NET 5 o .NET 6. Las aplicaciones Native AOT pueden confiar en la biblioteca entonces.

Encuentre problemas de AOT

Nuestra primera solicitud de soporte sobre la compatibilidad de AOT suena así:

Estamos considerando utilizar Docotic.Pdf compilado en WebAssembly. Produce advertencias de recorte cuando lo compilamos en AOT:
advertencia IL2104: El ensamblaje 'BitMiracle.Docotic.Pdf' produjo advertencias de ajuste. Para obtener más información, consulte https://aka.ms/dotnet-illink/libraries

No muy informativo. Reproduzcamos el problema y obtengamos más detalles sobre estas advertencias de ajuste.

Recibir advertencias de publicación

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

  1. Cree un proyecto de consola .NET 8 con PublishAot = true
  2. Añade una referencia a tu proyecto y utiliza algunos de sus tipos

Aquí está la configuración de csproj que utilizamos 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 "Advertencias de ajuste producidas por el ensamblaje 'X'".

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 ensamblaje que se está verificando. No es necesario cubrir todas las funciones de su proyecto para recibir advertencias de publicación. Usar un solo tipo es suficiente. Pero necesitas escribir más código para detectar errores de tiempo de ejecución.

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

Advertencia de análisis de AOT IL3050: Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): El uso del miembro 'System.Enum.GetValues(Type)' que tiene 'RequiresDynamicCodeAttribute' puede interrumpir la funcionalidad al compilar AOT.

Advertencia de análisis de recorte IL2026: LogProviders.Log4NetLogProvider.Log4NetLogger.GetCreateLoggingEvent(ParameterExpression,UnaryExpression,ParameterExpression,UnaryExpression,Type): El uso del miembro 'System.Linq.Expressions.Expression.Property(Expression,String)' que tiene 'RequiresUnreferencedCodeAttribute' puede interrumpir la funcionalidad al recortar el código de la aplicación.

Advertencia de análisis de recorte IL2057: LogProviders.LogProviderBase.FindType(String,String[]): Valor no reconocido pasado al parámetro 'typeName' del método 'System.Type.GetType(String)'.

Advertencia de análisis de recorte IL2070: LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String): 'this' argumento no satisface 'DynamicallyAccessedMemberTypes.PublicProperties' en la llamada a 'System.Type.GetProperty(String)'. El parámetro 'loggerType' del método 'LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)' no tiene anotaciones coincidentes. El valor de origen debe declarar al menos los mismos requisitos que los declarados en la ubicación de destino a la que está asignado.

Advertencia de análisis de recorte IL2075: LogProviders.Log4NetLogProvider.GetOpenNdcMethod(): 'this' argumento no satisface 'DynamicallyAccessedMemberTypes.PublicProperties' en la llamada a 'System.Type.GetProperty(String)'. El valor de retorno del método 'LogProviders.LogProviderBase.FindType(String,String)' no tiene anotaciones coincidentes. El valor de origen debe declarar al menos los mismos requisitos que los declarados en la ubicación de destino a la que está asignado.

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

Utilice analizadores de compatibilidad AOT

Microsoft proporciona analizadores Roslyn para encontrar problemas de AOT en proyectos .NET 7 o .NET

  1. Pueden resaltar problemas de AOT al escribir el código. Pero la publicación de una aplicación de prueba detecta más problemas.

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

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

Debe apuntar al marco .NET 7 o .NET 8 para utilizar dicho 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

¿Todos se relacionaron con la compatibilidad con AOT? Habíamos reconstruido el proyecto sin la propiedad IsAotCompatible. Aún así, 1036 errores.

Todos los errores 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 a una mejor compatibilidad con tipos de referencia que aceptan valores NULL. También hubo muchos errores CA1500 y CA1512 sobre los nuevos métodos auxiliares ArgumentOutOfRangeException.ThrowIfLessThan y ArgumentNullException.ThrowIfNull.

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

Para confirmar, agregamos 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 recorte. Y el análisis estático identificó los siguientes errores:

error IL3050: El uso del miembro 'System.Type.MakeGenericType(params Type[])' que tiene 'RequiresDynamicCodeAttribute' puede interrumpir la funcionalidad al compilar AOT. Es posible que el código nativo para esta creación de instancias no esté disponible en tiempo de ejecución.

error IL2057: Valor no reconocido pasado al parámetro 'typeName' del método 'System.Type.GetType(String)'. No es posible garantizar la disponibilidad del tipo de destino.

Entonces, los analizadores Roslyn funcionan y pueden encontrar algunos problemas. En nuestro caso no encontraron nada.

Detectar errores de tiempo de ejecución

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

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

En la práctica, la versión posterior a AOT aún puede funcionar de manera diferente. Habíamos arreglado todas las advertencias de ajuste y AOT. Todas las pruebas automáticas pasaron. La aplicación de prueba procesó documentos PDF correctamente. Y la versión AOT de la misma solicitud produjo documentos incorrectos.

La aplicación AOT todavía no funcionó

La publicación AOT eliminó por error algún código requerido. Creamos el problema La optimización elimina el código necesario. El equipo de .NET lo confirmó y solucionó rápidamente. Y agregamos una solución alternativa a nuestro código C# hasta que la solución esté disponible.

La moraleja de esta historia es que las advertencias de publicación y análisis estático pueden no ser suficientes. Utilice 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 Docotic.Pdf utiliza compresión de PDF, extracción de texto y otras funciones clave. Lo usamos así:

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

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

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.

Realizar pruebas fuera de la aplicación AOT

Aquí, utiliza un proyecto NUnit/xUnit/MSTest normal. Antes de crear este proyecto, publique la aplicación de prueba AOT. Las pruebas ejecutan la aplicación publicada utilizando el método Process.Start.

La principal ventaja es la separación de la infraestructura de prueba de la aplicación AOT. Puede utilizar todas las capacidades de los marcos de pruebas unitarias. Cualquier problema de compatibilidad de AOT en el marco de prueba no importa. También es más fácil reutilizar casos de prueba existentes.

La configuración de muestra del proyecto de prueba está disponible en GitHub. El proyecto Docotic.Tests contiene pruebas automáticas para las versiones administradas 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 utiliza el asistente "Publicar" 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í utilizamos el argumento --no-build. El proyecto ya está construido antes del evento posterior a la construcción. No necesitamos construirlo de nuevo.

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

Coloque 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 podrá utilizar la vista previa anticipada de MS Test. Lea el artículo Prueba de sus aplicaciones Native AOT para obtener más detalles.

El problema clave es que cualquier problema de AOT en el marco de pruebas hará que sus pruebas no sean confiables. Y solo está disponible un conjunto limitado de funciones del marco de prueba. Por eso preferimos colocar la infraestructura de prueba fuera de la aplicación AOT.

Reparar AOT y recortar advertencias

Evite suprimir las advertencias de publicación. Generalmente, suprimir es una solución incorrecta. Cuando suprime AOT o recorta advertencias, dice que su proyecto es compatible con AOT. Sin embargo, la causa fundamental de la advertencia sigue ahí. Cualquier advertencia suprimida podría provocar finalmente un fallo en tiempo de ejecución de su aplicación.

Intente eliminar o reescribir el código asociado con cada advertencia. La actualización de componentes de terceros también podría resultar útil.

Repasemos cómo arreglamos AOT y recortamos las advertencias en Docotic.Pdf. Ya viste algunos de ellos en la sección Recibir advertencias de publicación.

Recortar advertencias de LibLog

Usamos la biblioteca LibLog para iniciar sesión. LibLog se basa en la reflexión para detectar y utilizar marcos de registro populares. Es incompatible con AOT por diseño.

Actualmente, el paquete Microsoft.Extensions.Logging es el estándar para iniciar sesión en .NET. Y el desarrollo de LibLog ahora está congelado.

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

Sin advertencias de recorte

Un código de depuración en el complemento Docotic.Layout

En el complemento Docotic.Layout, utilizamos la reflexión para la depuración interna. Esto generó la advertencia IL2075.

El código correspondiente existe en la base del código, pero los clientes no pueden usarlo a través de la API pública. La solución es excluir el código de la configuración Release. Ahora se utiliza únicamente en la configuración Depurar.

Advertencia AOT IL3050 de BouncyCastle

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

Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): El uso del miembro 'System.Enum.GetValues(Type)' que tiene 'RequiresDynamicCodeAttribute' puede interrumpir la funcionalidad al compilar AOT. Es posible que no sea posible crear una matriz del tipo enumeración en tiempo de ejecución. Utilice la sobrecarga GetValues<TEnum> o el método GetValuesAsUnderlyingType en su lugar.

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

La solución más sencilla sería utilizar la sobrecarga Enum.GetValues<T>() en su lugar. La última versión de BouncyCastle ya lo utiliza. Lamentablemente, esta sobrecarga sólo está disponible en .NET 5 o posterior. Esta no es una opción para .NET Standard 2.0.

Profundizamos 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 se eliminaron los usos que ya no son necesarios de Enum.GetValues(Type).

Un problema extraño en la compilación ofuscada

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

<desconocido>:0: error: el símbolo '__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon' ya está definido

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

Los errores ocurrieron solo en la versión ofuscada. Podría ser un error en el ofuscador. Habíamos actualizado el ofuscador, pero todavía había errores.

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

  1. Deshabilite la ofuscación de 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 desde el paso uno. Compruebe si se producen errores. Continúe hasta encontrar los tipos que conducen a los errores.
  3. Deshabilite la ofuscación para la mitad de los miembros del tipo del paso dos. Compruebe si se producen errores. Continúe hasta encontrar los miembros que conducen a 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, aún se produjeron errores.

Analizamos el ensamblaje generado con ILSpy. Se encontraron algunos tipos inesperados generados por el compilador. ILSpy mostró usos de estos tipos de dicho código C#:

interface X
{
    void f();
}

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

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

No utilice GetCallingAssembly

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

Assembly.GetCallingAssembly() genera System.PlatformNotSupportedException: la operación no se admite en esta plataforma.

El método Assembly.GetCallingAssembly debe implementarse para Native AOT en .NET 9. Hasta que la solución esté lista, refactorizamos nuestro código para minimizar el impacto negativo. Ahora, usamos los atributos del ensamblado de llamada para validar la Licencia Application únicamente. Otros tipos de licencia son compatibles con AOT.

Marcar el código con atributos especiales

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

.NET proporciona atributos especiales para tales situaciones. Marca la API con atributos para informar a los clientes sobre problemas conocidos de AOT. Los usuarios de su API recibirán una advertencia cuando llamen 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 recortar.

Conclusión

La implementación Native AOT es un gran paso adelante en el mundo .NET. Puede escribir código C# normal y obtener una aplicación o biblioteca nativa. Estas aplicaciones suelen ser más rápidas y pueden ejecutarse sin tener instalado el tiempo de ejecución .NET. Incluso puede utilizar archivos DLL publicados en C, Rust u otros lenguajes de programación que no sean .NET.

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

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

  • Complemento Layout
  • Complemento Gdi
  • Complemento Logging

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

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