From dbf0f8f457f3bf86c8902610b0f64c0eb47402e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:26:29 +0000 Subject: [PATCH 1/9] Initial plan From 6850ac958d049bfb32db2bb785a704f197c97286 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:13:46 +0000 Subject: [PATCH 2/9] Replace AssemblyStore LZ4 compression with Zstd Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- Configuration.props | 2 - .../Tasks/CompressAssemblies.cs | 4 +- .../Utilities/AssemblyCompression.cs | 9 +-- .../Utilities/AssemblyStoreAssemblyInfo.cs | 2 +- .../Utilities/NativeRuntimeComponents.cs | 1 - .../Utilities/Zstd.cs | 74 +++++++++++++++++++ .../Xamarin.Android.Common.targets | 4 +- src/native/CMakeLists.txt | 1 - src/native/clr/host/CMakeLists.txt | 3 +- src/native/clr/host/assembly-store.cc | 19 ++--- src/native/clr/include/xamarin-app.hh | 2 +- .../include/runtime-base/timing-internal.hh | 2 +- .../common/include/runtime-base/zstd.hh | 18 +++++ src/native/common/lz4/CMakeLists.txt | 42 ----------- src/native/mono/monodroid/CMakeLists.txt | 3 +- .../mono/monodroid/embedded-assemblies.cc | 19 ++--- .../mono/xamarin-app-stub/xamarin-app.hh | 2 +- src/native/native.targets | 2 - .../assembly-store-reader.csproj | 1 - .../assembly-store-reader.csproj | 1 - .../decompress-assemblies.csproj | 1 - tools/decompress-assemblies/main.cs | 29 ++++++-- tools/tmt/ApkManagedTypeResolver.cs | 20 +++-- tools/tmt/tmt.csproj | 1 - 24 files changed, 159 insertions(+), 103 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs create mode 100644 src/native/common/include/runtime-base/zstd.hh delete mode 100644 src/native/common/lz4/CMakeLists.txt diff --git a/Configuration.props b/Configuration.props index f3f4619ff0e..3c21ea53d9a 100644 --- a/Configuration.props +++ b/Configuration.props @@ -99,7 +99,6 @@ $(MSBuildThisFileDirectory)external\sqlite $(MSBuildThisFileDirectory)external\libunwind $(BootstrapOutputDirectory)\libunwind - $(MSBuildThisFileDirectory)external\lz4 $(MSBuildThisFileDirectory) $(MSBuildThisFileDirectory)src-ThirdParty\ armeabi-v7a;x86 @@ -171,7 +170,6 @@ $([System.IO.Path]::GetFullPath ('$(SqliteSourceDirectory)')) $([System.IO.Path]::GetFullPath ('$(LibUnwindSourceDirectory)')) $([System.IO.Path]::GetFullPath ('$(LibUnwindGeneratedHeadersDirectory)')) - $([System.IO.Path]::GetFullPath ('$(LZ4SourceDirectory)')) net10.0 diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/CompressAssemblies.cs b/src/Xamarin.Android.Build.Tasks/Tasks/CompressAssemblies.cs index 039cfc0626c..9089a66c821 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/CompressAssemblies.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/CompressAssemblies.cs @@ -7,9 +7,9 @@ namespace Xamarin.Android.Tasks; /// -/// Compresses assemblies using LZ4 compression before placing them in the APK. +/// Compresses assemblies using Zstandard compression before placing them in the APK. /// Note this is independent of whether they are stored compressed with ZIP in the APK. -/// Our runtime bits will LZ4 decompress them at assembly load time. +/// Our runtime bits will Zstd decompress them at assembly load time. /// public class CompressAssemblies : AndroidTask { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs b/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs index ae852f42feb..f2d6b7ce4ce 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.IO; -using K4os.Compression.LZ4; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; @@ -45,7 +44,7 @@ public void SetData (string sourcePath, uint descriptorIndex) } } - const uint CompressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian + const uint CompressedDataMagic = 0x535A4158; // 'XAZS', little-endian static readonly ArrayPool bytePool = ArrayPool.Shared; @@ -78,8 +77,8 @@ static CompressionResult Compress (AssemblyData data, string outputFilePath) bytesRead = fs.Read (sourceBytes, 0, fileSize); } - destBytes = bytePool.Rent (LZ4Codec.MaximumOutputSize (bytesRead)); - int encodedLength = LZ4Codec.Encode (sourceBytes, 0, bytesRead, destBytes, 0, destBytes.Length, LZ4Level.L12_MAX); + destBytes = bytePool.Rent (Zstd.MaximumOutputSize (bytesRead)); + int encodedLength = Zstd.Compress (sourceBytes, bytesRead, destBytes); if (encodedLength < 0) return CompressionResult.EncodingFailed; @@ -154,7 +153,7 @@ public static bool TryGetDescriptorIndex (TaskLoggingHelper log, ITaskItem assem public static string GetCompressedAssemblyOutputPath (ITaskItem assembly, string compressedOutputDir) { var assemblyOutputDir = GetCompressedAssemblyOutputDirectory (assembly, compressedOutputDir); - return Path.Combine (assemblyOutputDir, $"{Path.GetFileName (assembly.ItemSpec)}.lz4"); + return Path.Combine (assemblyOutputDir, $"{Path.GetFileName (assembly.ItemSpec)}.zst"); } static string GetCompressedAssemblyOutputDirectory (ITaskItem assembly, string compressedOutputDir) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyStoreAssemblyInfo.cs b/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyStoreAssemblyInfo.cs index 590ca553c7d..a8c38d99a29 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyStoreAssemblyInfo.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyStoreAssemblyInfo.cs @@ -34,7 +34,7 @@ public AssemblyStoreAssemblyInfo (string sourceFilePath, ITaskItem assembly, boo throw new InvalidOperationException ("Internal error: info without assembly name"); } - if (name.EndsWith (".lz4", StringComparison.OrdinalIgnoreCase)) { + if (name.EndsWith (".zst", StringComparison.OrdinalIgnoreCase)) { name = Path.GetFileNameWithoutExtension (name); } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs index 4773d0cd1c1..141bab21947 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs @@ -125,7 +125,6 @@ public NativeRuntimeComponents (ITaskItem[]? monoComponents) new AndroidArchive ("libruntime-base-common-release.a"), new AndroidArchive ("libruntime-base-release.a"), new AndroidArchive ("libxa-java-interop-release.a"), - new AndroidArchive ("libxa-lz4-release.a"), new AndroidArchive ("libxa-shared-bits-release.a"), new AndroidArchive ("libxamarin-startup-release.a"), diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs b/src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs new file mode 100644 index 00000000000..bd81780dc69 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs @@ -0,0 +1,74 @@ +#nullable enable +using System; +using System.Runtime.InteropServices; + +namespace Xamarin.Android.Tasks +{ + /// + /// Minimal managed wrapper around the Zstandard compression functions exported by + /// libSystem.IO.Compression.Native, which ships in the .NET runtime pack. + /// We use it to compress assemblies that are placed in the assembly store; the native + /// runtime decompresses them at load time using the same library. + /// + static class Zstd + { + // libSystem.IO.Compression.Native exports the raw zstd entry points (no prefix). + const string ZstdLibrary = "System.IO.Compression.Native"; + + // ZSTD_cParameter value for ZSTD_c_compressionLevel (see zstd.h). + const int ZSTD_c_compressionLevel = 100; + + [DllImport (ZstdLibrary)] + static extern UIntPtr ZSTD_compressBound (UIntPtr srcSize); + + [DllImport (ZstdLibrary)] + static extern IntPtr ZSTD_createCCtx (); + + [DllImport (ZstdLibrary)] + static extern UIntPtr ZSTD_freeCCtx (IntPtr cctx); + + [DllImport (ZstdLibrary)] + static extern UIntPtr ZSTD_CCtx_setParameter (IntPtr cctx, int param, int value); + + [DllImport (ZstdLibrary)] + static extern UIntPtr ZSTD_compress2 (IntPtr cctx, byte[] dst, UIntPtr dstCapacity, byte[] src, UIntPtr srcSize); + + [DllImport (ZstdLibrary)] + static extern uint ZSTD_isError (UIntPtr code); + + [DllImport (ZstdLibrary)] + static extern int ZSTD_maxCLevel (); + + /// + /// Returns the maximum size that compressed data of bytes can occupy. + /// + public static int MaximumOutputSize (int inputSize) + { + return checked ((int) (ulong) ZSTD_compressBound ((UIntPtr) (uint) inputSize)); + } + + /// + /// Compresses bytes from into + /// using the maximum compression level. Returns the number of + /// bytes written to , or -1 if compression failed. + /// + public static int Compress (byte[] input, int inputLength, byte[] output) + { + IntPtr cctx = ZSTD_createCCtx (); + if (cctx == IntPtr.Zero) + return -1; + + try { + ZSTD_CCtx_setParameter (cctx, ZSTD_c_compressionLevel, ZSTD_maxCLevel ()); + + UIntPtr result = ZSTD_compress2 (cctx, output, (UIntPtr) (uint) output.Length, input, (UIntPtr) (uint) inputLength); + if (ZSTD_isError (result) != 0) + return -1; + + return checked ((int) (ulong) result); + } finally { + ZSTD_freeCCtx (cctx); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index ea3cf49c141..0af5e923d83 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2150,7 +2150,7 @@ because xbuild doesn't support framework reference assemblies. This dependency chain should be addressed in a separate PR. --> -#if defined (HAVE_LZ4) -#include -#endif - #include #include #include #include #include #include +#include using namespace xamarin::android; @@ -26,7 +23,7 @@ auto AssemblyStore::get_assembly_data (AssemblyStoreSingleAssemblyRuntimeData co uint8_t *assembly_data = nullptr; uint32_t assembly_data_size = 0; -#if defined (HAVE_LZ4) && defined (RELEASE) +#if defined (RELEASE) auto header = reinterpret_cast(e.image_data); if (header->magic == COMPRESSED_DATA_MAGIC) { log_debug (LOG_ASSEMBLY, "Decompressing assembly '{}' from the assembly store"sv, name); @@ -114,20 +111,20 @@ auto AssemblyStore::get_assembly_data (AssemblyStoreSingleAssemblyRuntimeData co } const char *data_start = pointer_add(e.image_data, sizeof(CompressedAssemblyHeader)); - int ret = LZ4_decompress_safe (data_start, reinterpret_cast(data_buffer), static_cast(assembly_data_size), static_cast(cad.uncompressed_file_size)); + size_t ret = ZSTD_decompress (data_buffer, cad.uncompressed_file_size, data_start, assembly_data_size); - if (ret < 0) { + if (ZSTD_isError (ret)) { Helpers::abort_application ( LOG_ASSEMBLY, std::format ( - "Decompression of assembly {} failed with code {}"sv, + "Decompression of assembly {} failed: {}"sv, name, - ret + ZSTD_getErrorName (ret) ) ); } - if (static_cast(ret) != cad.uncompressed_file_size) { + if (ret != cad.uncompressed_file_size) { Helpers::abort_application ( LOG_ASSEMBLY, std::format ( @@ -147,7 +144,7 @@ auto AssemblyStore::get_assembly_data (AssemblyStoreSingleAssemblyRuntimeData co set_assembly_data_and_size (data_buffer, cad.uncompressed_file_size, assembly_data, assembly_data_size); } else -#endif // def HAVE_LZ4 && def RELEASE +#endif // def RELEASE { log_debug (LOG_ASSEMBLY, "Assembly '{}' is not compressed in the assembly store"sv, name); diff --git a/src/native/clr/include/xamarin-app.hh b/src/native/clr/include/xamarin-app.hh index c2140e2c882..39fbb97f822 100644 --- a/src/native/clr/include/xamarin-app.hh +++ b/src/native/clr/include/xamarin-app.hh @@ -9,7 +9,7 @@ #include static constexpr uint64_t FORMAT_TAG = 0x00045E6972616D58; // 'Xmari^XY' where XY is the format version -static constexpr uint32_t COMPRESSED_DATA_MAGIC = 0x5A4C4158; // 'XALZ', little-endian +static constexpr uint32_t COMPRESSED_DATA_MAGIC = 0x535A4158; // 'XAZS', little-endian static constexpr uint32_t ASSEMBLY_STORE_MAGIC = 0x41424158; // 'XABA', little-endian // The highest bit of assembly store version is a 64-bit ABI flag diff --git a/src/native/common/include/runtime-base/timing-internal.hh b/src/native/common/include/runtime-base/timing-internal.hh index 31099b86322..5bd95f11af4 100644 --- a/src/native/common/include/runtime-base/timing-internal.hh +++ b/src/native/common/include/runtime-base/timing-internal.hh @@ -430,7 +430,7 @@ namespace xamarin::android { switch (kind) { case TimingEventKind::AssemblyDecompression: - append_desc ("LZ4 decompression time for "sv); + append_desc ("Zstd decompression time for "sv); return; case TimingEventKind::AssemblyLoad: diff --git a/src/native/common/include/runtime-base/zstd.hh b/src/native/common/include/runtime-base/zstd.hh new file mode 100644 index 00000000000..bbab4c91471 --- /dev/null +++ b/src/native/common/include/runtime-base/zstd.hh @@ -0,0 +1,18 @@ +// Dear Emacs, this is a -*- C++ -*- header +#pragma once + +#include + +// +// Minimal declarations for the Zstandard decompression functions that are exported by +// `libSystem.IO.Compression.Native`, which ships in the .NET runtime pack and is linked +// into our runtime. The assembly store compresses assemblies with Zstd at build time and +// we decompress them here at load time. +// +// We declare only the few entry points we need instead of pulling in `zstd.h`. +// +extern "C" { + size_t ZSTD_decompress (void *dst, size_t dst_capacity, const void *src, size_t compressed_size) noexcept; + unsigned ZSTD_isError (size_t code) noexcept; + const char* ZSTD_getErrorName (size_t code) noexcept; +} diff --git a/src/native/common/lz4/CMakeLists.txt b/src/native/common/lz4/CMakeLists.txt deleted file mode 100644 index 57c8a717d01..00000000000 --- a/src/native/common/lz4/CMakeLists.txt +++ /dev/null @@ -1,42 +0,0 @@ -set(LIB_NAME xa-lz4) -set(LIB_ALIAS xa::lz4) - -set(LZ4_SRC_DIR "${EXTERNAL_DIR}/lz4/lib") -set(LZ4_INCLUDE_DIR ${LZ4_SRC_DIR}) - -set(LZ4_SOURCES - ${LZ4_SRC_DIR}/lz4.c -) - -add_library( - ${LIB_NAME} - STATIC - ${LZ4_SOURCES} -) -set_static_library_suffix(${LIB_NAME}) - -add_library(${LIB_ALIAS} ALIAS ${LIB_NAME}) - -target_compile_definitions( - ${LIB_NAME} - PRIVATE - # Ugly, but this is the only way to change LZ4 symbols visibility without modifying lz4.h - "LZ4LIB_VISIBILITY=__attribute__ ((visibility (\"hidden\")))" - XXH_NAMESPACE=LZ4_ -) - -target_include_directories( - ${LIB_NAME} - PUBLIC - "$" -) - -if(DEBUG_BUILD) - set_target_properties( - ${LIB_NAME} - PROPERTIES - ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" - ) -endif() - -xa_add_compile_definitions(${LIB_NAME}) diff --git a/src/native/mono/monodroid/CMakeLists.txt b/src/native/mono/monodroid/CMakeLists.txt index 888b87d3d98..43da300d837 100644 --- a/src/native/mono/monodroid/CMakeLists.txt +++ b/src/native/mono/monodroid/CMakeLists.txt @@ -152,7 +152,6 @@ macro(lib_target_options TARGET_NAME) ${TARGET_NAME} PRIVATE HAVE_CONFIG_H - HAVE_LZ4 JI_DLL_EXPORT JI_NO_VISIBILITY MONO_DLL_EXPORT @@ -231,7 +230,7 @@ macro(lib_target_options TARGET_NAME) xa::runtime-base xa::java-interop xa::pinvoke-override-precompiled - xa::lz4 + -lSystem.IO.Compression.Native -lmonosgen-2.0 -landroid -llog diff --git a/src/native/mono/monodroid/embedded-assemblies.cc b/src/native/mono/monodroid/embedded-assemblies.cc index c3aa3488aa8..08099f65324 100644 --- a/src/native/mono/monodroid/embedded-assemblies.cc +++ b/src/native/mono/monodroid/embedded-assemblies.cc @@ -15,10 +15,6 @@ #include #include -#if defined (HAVE_LZ4) -#include -#endif - #include #include #include @@ -37,6 +33,7 @@ #include "startup-aware-lock.hh" #include #include +#include using namespace xamarin::android; using namespace xamarin::android::internal; @@ -82,7 +79,7 @@ EmbeddedAssemblies::set_assembly_data_and_size (uint8_t* source_assembly_data, u [[gnu::always_inline]] void EmbeddedAssemblies::get_assembly_data (uint8_t *data, uint32_t data_size, [[maybe_unused]] const char *name, uint8_t*& assembly_data, uint32_t& assembly_data_size) noexcept { -#if defined (HAVE_LZ4) && defined (RELEASE) +#if defined (RELEASE) auto header = reinterpret_cast(data); if (header->magic == COMPRESSED_DATA_MAGIC) { if (compressed_assembly_count == 0) [[unlikely]] { @@ -156,20 +153,20 @@ EmbeddedAssemblies::get_assembly_data (uint8_t *data, uint32_t data_size, [[mayb } const char *data_start = pointer_add(data, sizeof(CompressedAssemblyHeader)); - int ret = LZ4_decompress_safe (data_start, reinterpret_cast(data_buffer), static_cast(assembly_data_size), static_cast(cad.uncompressed_file_size)); + size_t ret = ZSTD_decompress (data_buffer, cad.uncompressed_file_size, data_start, assembly_data_size); - if (ret < 0) { + if (ZSTD_isError (ret)) { Helpers::abort_application ( LOG_ASSEMBLY, std::format ( - "Decompression of assembly {} failed with code {}", + "Decompression of assembly {} failed: {}", optional_string (name), - ret + ZSTD_getErrorName (ret) ) ); } - if (static_cast(ret) != cad.uncompressed_file_size) { + if (ret != cad.uncompressed_file_size) { Helpers::abort_application ( LOG_ASSEMBLY, std::format ( @@ -185,7 +182,7 @@ EmbeddedAssemblies::get_assembly_data (uint8_t *data, uint32_t data_size, [[mayb set_assembly_data_and_size (data_buffer, cad.uncompressed_file_size, assembly_data, assembly_data_size); } else -#endif // def HAVE_LZ4 && def RELEASE +#endif // def RELEASE { set_assembly_data_and_size (data, data_size, assembly_data, assembly_data_size); } diff --git a/src/native/mono/xamarin-app-stub/xamarin-app.hh b/src/native/mono/xamarin-app-stub/xamarin-app.hh index c0e3d3012d6..2a21c1e827b 100644 --- a/src/native/mono/xamarin-app-stub/xamarin-app.hh +++ b/src/native/mono/xamarin-app-stub/xamarin-app.hh @@ -12,7 +12,7 @@ #include static constexpr uint64_t FORMAT_TAG = 0x00035E6972616D58; // 'Xmari^XY' where XY is the format version -static constexpr uint32_t COMPRESSED_DATA_MAGIC = 0x5A4C4158; // 'XALZ', little-endian +static constexpr uint32_t COMPRESSED_DATA_MAGIC = 0x535A4158; // 'XAZS', little-endian static constexpr uint32_t ASSEMBLY_STORE_MAGIC = 0x41424158; // 'XABA', little-endian // The highest bit of assembly store version is a 64-bit ABI flag diff --git a/src/native/native.targets b/src/native/native.targets index 01a2522e38c..80c87e42131 100644 --- a/src/native/native.targets +++ b/src/native/native.targets @@ -36,7 +36,6 @@ <_ConfigureRuntimesInputs Include="CMakeLists.txt" /> <_ConfigureRuntimesInputs Include="common\java-interop\CMakeLists.txt" /> <_ConfigureRuntimesInputs Include="common\libunwind\CMakeLists.txt" /> - <_ConfigureRuntimesInputs Include="common\lz4\CMakeLists.txt" /> <_ConfigureRuntimesInputs Include="common\runtime-base\CMakeLists.txt" /> <_ConfigureRuntimesOutputs Include="@(AndroidSupportedTargetJitAbi->'$(FlavorIntermediateOutputPath)\%(AndroidRID)-Debug\CMakeCache.txt')" /> @@ -217,7 +216,6 @@ <_RuntimeSources Include="common\archive-dso-stub\*.cc" /> <_RuntimeSources Include="common\include\**\*.hh" /> <_RuntimeSources Include="common\runtime-base\*.cc" /> - <_RuntimeSources Include="$(LZ4SourceFullPath)\lib\lz4.c;$(LZ4SourceFullPath)\lib\lz4.h" /> diff --git a/tools/assembly-store-reader-mk2/assembly-store-reader.csproj b/tools/assembly-store-reader-mk2/assembly-store-reader.csproj index ec4c90fecb1..1a760876dfc 100644 --- a/tools/assembly-store-reader-mk2/assembly-store-reader.csproj +++ b/tools/assembly-store-reader-mk2/assembly-store-reader.csproj @@ -17,7 +17,6 @@ - diff --git a/tools/assembly-store-reader/assembly-store-reader.csproj b/tools/assembly-store-reader/assembly-store-reader.csproj index 9b0e604b1da..c2b4c58ff21 100644 --- a/tools/assembly-store-reader/assembly-store-reader.csproj +++ b/tools/assembly-store-reader/assembly-store-reader.csproj @@ -17,7 +17,6 @@ - diff --git a/tools/decompress-assemblies/decompress-assemblies.csproj b/tools/decompress-assemblies/decompress-assemblies.csproj index 1f789958973..e6577c6e52f 100644 --- a/tools/decompress-assemblies/decompress-assemblies.csproj +++ b/tools/decompress-assemblies/decompress-assemblies.csproj @@ -22,7 +22,6 @@ - diff --git a/tools/decompress-assemblies/main.cs b/tools/decompress-assemblies/main.cs index ba7d9ad4b2c..b68b69f8a1d 100644 --- a/tools/decompress-assemblies/main.cs +++ b/tools/decompress-assemblies/main.cs @@ -1,8 +1,8 @@ using System; using System.Buffers; using System.IO; +using System.Runtime.InteropServices; -using K4os.Compression.LZ4; using Xamarin.Tools.Zip; using Xamarin.Android.AssemblyStore; @@ -10,7 +10,24 @@ namespace Xamarin.Android.Tools.DecompressAssemblies { class App { - const uint CompressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian + const uint CompressedDataMagic = 0x535A4158; // 'XAZS', little-endian + + // Zstd decompression entry points exported by libSystem.IO.Compression.Native (from the .NET runtime pack). + [DllImport ("System.IO.Compression.Native")] + static extern UIntPtr ZSTD_decompress (byte[] dst, UIntPtr dstCapacity, byte[] src, UIntPtr srcSize); + + [DllImport ("System.IO.Compression.Native")] + static extern uint ZSTD_isError (UIntPtr code); + + // Decompresses 'srcLength' bytes from 'src' into 'dst' (which must be 'dstLength' bytes long). + // Returns the number of bytes written, or -1 on failure. + static int ZstdDecompress (byte[] src, int srcLength, byte[] dst, int dstLength) + { + UIntPtr result = ZSTD_decompress (dst, (UIntPtr) (uint) dstLength, src, (UIntPtr) (uint) srcLength); + if (ZSTD_isError (result) != 0) + return -1; + return (int) (ulong) result; + } static readonly ArrayPool bytePool = ArrayPool.Shared; @@ -30,8 +47,8 @@ static bool UncompressDLL (Stream inputStream, string fileName, string filePath, Console.WriteLine ($"Processing {fileName}"); // - // LZ4 compressed assembly header format: - // uint magic; // 0x5A4C4158; 'XALZ', little-endian + // Zstd compressed assembly header format: + // uint magic; // 0x535A4158; 'XAZS', little-endian // uint descriptor_index; // Index into an internal assembly descriptor table // uint uncompressed_length; // Size of assembly, uncompressed // @@ -46,9 +63,9 @@ static bool UncompressDLL (Stream inputStream, string fileName, string filePath, reader.Read (sourceBytes, 0, inputLength); byte[] assemblyBytes = bytePool.Rent ((int)decompressedLength); - int decoded = LZ4Codec.Decode (sourceBytes, 0, inputLength, assemblyBytes, 0, (int)decompressedLength); + int decoded = ZstdDecompress (sourceBytes, inputLength, assemblyBytes, (int)decompressedLength); if (decoded != (int)decompressedLength) { - Console.Error.WriteLine ($" Failed to decompress LZ4 data of {fileName} (decoded: {decoded})"); + Console.Error.WriteLine ($" Failed to decompress Zstd data of {fileName} (decoded: {decoded})"); retVal = false; } else { string? outputDir = Path.GetDirectoryName (outputFile); diff --git a/tools/tmt/ApkManagedTypeResolver.cs b/tools/tmt/ApkManagedTypeResolver.cs index 5f519e07275..d1259ef9363 100644 --- a/tools/tmt/ApkManagedTypeResolver.cs +++ b/tools/tmt/ApkManagedTypeResolver.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; -using K4os.Compression.LZ4; using Mono.Cecil; using Xamarin.Android.AssemblyStore; using Xamarin.Tools.Zip; @@ -11,7 +11,14 @@ namespace tmt { class ApkManagedTypeResolver : ManagedTypeResolver { - const uint CompressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian + const uint CompressedDataMagic = 0x535A4158; // 'XAZS', little-endian + + // Zstd decompression entry points exported by libSystem.IO.Compression.Native (from the .NET runtime pack). + [DllImport ("System.IO.Compression.Native")] + static extern UIntPtr ZSTD_decompress (byte[] dst, UIntPtr dstCapacity, byte[] src, UIntPtr srcSize); + + [DllImport ("System.IO.Compression.Native")] + static extern uint ZSTD_isError (UIntPtr code); readonly Dictionary? individualAssemblies; readonly Dictionary? blobAssemblies; @@ -133,8 +140,8 @@ protected override AssemblyDefinition ReadAssembly (string assemblyPath) Stream stream = GetAssemblyStream (assemblyPath); // - // LZ4 compressed assembly header format: - // uint magic; // 0x5A4C4158; 'XALZ', little-endian + // Zstd compressed assembly header format: + // uint magic; // 0x535A4158; 'XAZS', little-endian // uint descriptor_index; // Index into an internal assembly descriptor table // uint uncompressed_length; // Size of assembly, uncompressed // @@ -149,9 +156,10 @@ protected override AssemblyDefinition ReadAssembly (string assemblyPath) reader.Read (sourceBytes, 0, inputLength); assemblyBytes = Utilities.BytePool.Rent ((int)decompressedLength); - int decoded = LZ4Codec.Decode (sourceBytes, 0, inputLength, assemblyBytes, 0, (int)decompressedLength); + UIntPtr decodedResult = ZSTD_decompress (assemblyBytes, (UIntPtr)decompressedLength, sourceBytes, (UIntPtr)(uint)inputLength); + int decoded = ZSTD_isError (decodedResult) != 0 ? -1 : (int)(ulong)decodedResult; if (decoded != (int)decompressedLength) { - throw new InvalidOperationException ($"Failed to decompress LZ4 data of {assemblyPath} (decoded: {decoded})"); + throw new InvalidOperationException ($"Failed to decompress Zstd data of {assemblyPath} (decoded: {decoded})"); } Utilities.BytePool.Return (sourceBytes); } diff --git a/tools/tmt/tmt.csproj b/tools/tmt/tmt.csproj index bf31f337de7..92e23881bed 100644 --- a/tools/tmt/tmt.csproj +++ b/tools/tmt/tmt.csproj @@ -25,7 +25,6 @@ - From ed3ba4cddfeb6438d27f67739c66317a60fe5a9a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 25 Jun 2026 03:09:32 +0200 Subject: [PATCH 3/9] Update CoreCLR APK size descriptors Refresh BuildReleaseArm64 CoreCLR APK descriptors from PR CI after switching assembly-store compression to Zstd. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc | 10 +++++----- .../Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc index 0d3d9720ca1..4ff6b6b482a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc @@ -8,16 +8,16 @@ "Size": 402352 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 3461344 + "Size": 2482600 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2804464 + "Size": 2824392 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4872088 + "Size": 4890432 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 1325808 + "Size": 1265504 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72112 @@ -59,5 +59,5 @@ "Size": 1904 } }, - "PackageSize": 7497147 + "PackageSize": 7026107 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc index f156471b0da..d63fb8ef4c2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc @@ -32,16 +32,16 @@ "Size": 2396 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 14137920 + "Size": 9622600 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2804464 + "Size": 2824392 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4872088 + "Size": 4890432 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 1325808 + "Size": 1265504 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72112 @@ -2234,5 +2234,5 @@ "Size": 794696 } }, - "PackageSize": 20778573 + "PackageSize": 18730573 } \ No newline at end of file From d05c82a39de39a27fe77515d9ea2ce95fae140c8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 25 Jun 2026 09:18:25 +0200 Subject: [PATCH 4/9] Use LibraryImport in Zstd tools Switch the net10.0 diagnostic tools from DllImport to source-generated LibraryImport for Zstd decompression entry points. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../decompress-assemblies.csproj | 1 + tools/decompress-assemblies/main.cs | 23 +++++++++++-------- tools/tmt/ApkManagedTypeResolver.cs | 20 ++++++++++------ 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/tools/decompress-assemblies/decompress-assemblies.csproj b/tools/decompress-assemblies/decompress-assemblies.csproj index e6577c6e52f..881d8ba5fd4 100644 --- a/tools/decompress-assemblies/decompress-assemblies.csproj +++ b/tools/decompress-assemblies/decompress-assemblies.csproj @@ -11,6 +11,7 @@ ../../bin/$(Configuration)/bin Exe true + true enable diff --git a/tools/decompress-assemblies/main.cs b/tools/decompress-assemblies/main.cs index b68b69f8a1d..3e606196849 100644 --- a/tools/decompress-assemblies/main.cs +++ b/tools/decompress-assemblies/main.cs @@ -8,25 +8,30 @@ namespace Xamarin.Android.Tools.DecompressAssemblies { - class App + partial class App { const uint CompressedDataMagic = 0x535A4158; // 'XAZS', little-endian // Zstd decompression entry points exported by libSystem.IO.Compression.Native (from the .NET runtime pack). - [DllImport ("System.IO.Compression.Native")] - static extern UIntPtr ZSTD_decompress (byte[] dst, UIntPtr dstCapacity, byte[] src, UIntPtr srcSize); + [LibraryImport ("System.IO.Compression.Native")] + private static unsafe partial UIntPtr ZSTD_decompress (byte* dst, UIntPtr dstCapacity, byte* src, UIntPtr srcSize); - [DllImport ("System.IO.Compression.Native")] - static extern uint ZSTD_isError (UIntPtr code); + [LibraryImport ("System.IO.Compression.Native")] + private static partial uint ZSTD_isError (UIntPtr code); // Decompresses 'srcLength' bytes from 'src' into 'dst' (which must be 'dstLength' bytes long). // Returns the number of bytes written, or -1 on failure. static int ZstdDecompress (byte[] src, int srcLength, byte[] dst, int dstLength) { - UIntPtr result = ZSTD_decompress (dst, (UIntPtr) (uint) dstLength, src, (UIntPtr) (uint) srcLength); - if (ZSTD_isError (result) != 0) - return -1; - return (int) (ulong) result; + unsafe { + fixed (byte* dstPtr = dst) + fixed (byte* srcPtr = src) { + UIntPtr result = ZSTD_decompress (dstPtr, (UIntPtr) (uint) dstLength, srcPtr, (UIntPtr) (uint) srcLength); + if (ZSTD_isError (result) != 0) + return -1; + return (int) (ulong) result; + } + } } static readonly ArrayPool bytePool = ArrayPool.Shared; diff --git a/tools/tmt/ApkManagedTypeResolver.cs b/tools/tmt/ApkManagedTypeResolver.cs index d1259ef9363..571220a9a11 100644 --- a/tools/tmt/ApkManagedTypeResolver.cs +++ b/tools/tmt/ApkManagedTypeResolver.cs @@ -9,16 +9,16 @@ namespace tmt { - class ApkManagedTypeResolver : ManagedTypeResolver + partial class ApkManagedTypeResolver : ManagedTypeResolver { const uint CompressedDataMagic = 0x535A4158; // 'XAZS', little-endian // Zstd decompression entry points exported by libSystem.IO.Compression.Native (from the .NET runtime pack). - [DllImport ("System.IO.Compression.Native")] - static extern UIntPtr ZSTD_decompress (byte[] dst, UIntPtr dstCapacity, byte[] src, UIntPtr srcSize); + [LibraryImport ("System.IO.Compression.Native")] + private static unsafe partial UIntPtr ZSTD_decompress (byte* dst, UIntPtr dstCapacity, byte* src, UIntPtr srcSize); - [DllImport ("System.IO.Compression.Native")] - static extern uint ZSTD_isError (UIntPtr code); + [LibraryImport ("System.IO.Compression.Native")] + private static partial uint ZSTD_isError (UIntPtr code); readonly Dictionary? individualAssemblies; readonly Dictionary? blobAssemblies; @@ -156,8 +156,14 @@ protected override AssemblyDefinition ReadAssembly (string assemblyPath) reader.Read (sourceBytes, 0, inputLength); assemblyBytes = Utilities.BytePool.Rent ((int)decompressedLength); - UIntPtr decodedResult = ZSTD_decompress (assemblyBytes, (UIntPtr)decompressedLength, sourceBytes, (UIntPtr)(uint)inputLength); - int decoded = ZSTD_isError (decodedResult) != 0 ? -1 : (int)(ulong)decodedResult; + int decoded; + unsafe { + fixed (byte* assemblyPtr = assemblyBytes) + fixed (byte* sourcePtr = sourceBytes) { + UIntPtr decodedResult = ZSTD_decompress (assemblyPtr, (UIntPtr)decompressedLength, sourcePtr, (UIntPtr)(uint)inputLength); + decoded = ZSTD_isError (decodedResult) != 0 ? -1 : (int)(ulong)decodedResult; + } + } if (decoded != (int)decompressedLength) { throw new InvalidOperationException ($"Failed to decompress Zstd data of {assemblyPath} (decoded: {decoded})"); } From 15c55884c87b331248d89aa4ecbc153164e9e0c2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 25 Jun 2026 09:22:34 +0200 Subject: [PATCH 5/9] Use BCL ZstandardDecoder in tools Retarget the diagnostic tools to net11.0 so they can use System.IO.Compression.ZstandardDecoder instead of native Zstd P/Invokes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../decompress-assemblies.csproj | 3 +-- tools/decompress-assemblies/main.cs | 24 +++++-------------- tools/tmt/ApkManagedTypeResolver.cs | 23 +++++------------- tools/tmt/tmt.csproj | 2 +- 4 files changed, 14 insertions(+), 38 deletions(-) diff --git a/tools/decompress-assemblies/decompress-assemblies.csproj b/tools/decompress-assemblies/decompress-assemblies.csproj index 881d8ba5fd4..cf59128cbc2 100644 --- a/tools/decompress-assemblies/decompress-assemblies.csproj +++ b/tools/decompress-assemblies/decompress-assemblies.csproj @@ -4,14 +4,13 @@ Microsoft Corporation 2021 Microsoft Corporation 0.0.1 - $(DotNetStableTargetFramework) + $(DotNetTargetFramework) false Xamarin.Android.Tools.DecompressAssemblies decompress-assemblies ../../bin/$(Configuration)/bin Exe true - true enable diff --git a/tools/decompress-assemblies/main.cs b/tools/decompress-assemblies/main.cs index 3e606196849..2fd2cb95107 100644 --- a/tools/decompress-assemblies/main.cs +++ b/tools/decompress-assemblies/main.cs @@ -1,37 +1,25 @@ using System; using System.Buffers; using System.IO; -using System.Runtime.InteropServices; using Xamarin.Tools.Zip; using Xamarin.Android.AssemblyStore; +using ZstandardDecoder = System.IO.Compression.ZstandardDecoder; namespace Xamarin.Android.Tools.DecompressAssemblies { - partial class App + class App { const uint CompressedDataMagic = 0x535A4158; // 'XAZS', little-endian - // Zstd decompression entry points exported by libSystem.IO.Compression.Native (from the .NET runtime pack). - [LibraryImport ("System.IO.Compression.Native")] - private static unsafe partial UIntPtr ZSTD_decompress (byte* dst, UIntPtr dstCapacity, byte* src, UIntPtr srcSize); - - [LibraryImport ("System.IO.Compression.Native")] - private static partial uint ZSTD_isError (UIntPtr code); - // Decompresses 'srcLength' bytes from 'src' into 'dst' (which must be 'dstLength' bytes long). // Returns the number of bytes written, or -1 on failure. static int ZstdDecompress (byte[] src, int srcLength, byte[] dst, int dstLength) { - unsafe { - fixed (byte* dstPtr = dst) - fixed (byte* srcPtr = src) { - UIntPtr result = ZSTD_decompress (dstPtr, (UIntPtr) (uint) dstLength, srcPtr, (UIntPtr) (uint) srcLength); - if (ZSTD_isError (result) != 0) - return -1; - return (int) (ulong) result; - } - } + return ZstandardDecoder.TryDecompress ( + src.AsSpan (0, srcLength), + dst.AsSpan (0, dstLength), + out int bytesWritten) ? bytesWritten : -1; } static readonly ArrayPool bytePool = ArrayPool.Shared; diff --git a/tools/tmt/ApkManagedTypeResolver.cs b/tools/tmt/ApkManagedTypeResolver.cs index 571220a9a11..23b539d5167 100644 --- a/tools/tmt/ApkManagedTypeResolver.cs +++ b/tools/tmt/ApkManagedTypeResolver.cs @@ -1,25 +1,18 @@ using System; using System.Collections.Generic; using System.IO; -using System.Runtime.InteropServices; using Mono.Cecil; using Xamarin.Android.AssemblyStore; using Xamarin.Tools.Zip; +using ZstandardDecoder = System.IO.Compression.ZstandardDecoder; namespace tmt { - partial class ApkManagedTypeResolver : ManagedTypeResolver + class ApkManagedTypeResolver : ManagedTypeResolver { const uint CompressedDataMagic = 0x535A4158; // 'XAZS', little-endian - // Zstd decompression entry points exported by libSystem.IO.Compression.Native (from the .NET runtime pack). - [LibraryImport ("System.IO.Compression.Native")] - private static unsafe partial UIntPtr ZSTD_decompress (byte* dst, UIntPtr dstCapacity, byte* src, UIntPtr srcSize); - - [LibraryImport ("System.IO.Compression.Native")] - private static partial uint ZSTD_isError (UIntPtr code); - readonly Dictionary? individualAssemblies; readonly Dictionary? blobAssemblies; readonly ZipArchive apk; @@ -156,14 +149,10 @@ protected override AssemblyDefinition ReadAssembly (string assemblyPath) reader.Read (sourceBytes, 0, inputLength); assemblyBytes = Utilities.BytePool.Rent ((int)decompressedLength); - int decoded; - unsafe { - fixed (byte* assemblyPtr = assemblyBytes) - fixed (byte* sourcePtr = sourceBytes) { - UIntPtr decodedResult = ZSTD_decompress (assemblyPtr, (UIntPtr)decompressedLength, sourcePtr, (UIntPtr)(uint)inputLength); - decoded = ZSTD_isError (decodedResult) != 0 ? -1 : (int)(ulong)decodedResult; - } - } + int decoded = ZstandardDecoder.TryDecompress ( + sourceBytes.AsSpan (0, inputLength), + assemblyBytes.AsSpan (0, (int)decompressedLength), + out int bytesWritten) ? bytesWritten : -1; if (decoded != (int)decompressedLength) { throw new InvalidOperationException ($"Failed to decompress Zstd data of {assemblyPath} (decoded: {decoded})"); } diff --git a/tools/tmt/tmt.csproj b/tools/tmt/tmt.csproj index 92e23881bed..4c1ba65babd 100644 --- a/tools/tmt/tmt.csproj +++ b/tools/tmt/tmt.csproj @@ -4,7 +4,7 @@ Microsoft Corporation 2020 Microsoft Corporation 0.0.1 - $(DotNetStableTargetFramework) + $(DotNetTargetFramework) false ../../bin/$(Configuration)/bin/typemap-tool Exe From 7b59da5f1e5778dbfb696bec1295d4e1cff0c81d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 25 Jun 2026 09:27:28 +0200 Subject: [PATCH 6/9] Inline ZstandardDecoder call Remove the small decompress-assemblies helper and call ZstandardDecoder.TryDecompress directly at the compressed payload site. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/decompress-assemblies/main.cs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tools/decompress-assemblies/main.cs b/tools/decompress-assemblies/main.cs index 2fd2cb95107..5efe0837a7e 100644 --- a/tools/decompress-assemblies/main.cs +++ b/tools/decompress-assemblies/main.cs @@ -12,16 +12,6 @@ class App { const uint CompressedDataMagic = 0x535A4158; // 'XAZS', little-endian - // Decompresses 'srcLength' bytes from 'src' into 'dst' (which must be 'dstLength' bytes long). - // Returns the number of bytes written, or -1 on failure. - static int ZstdDecompress (byte[] src, int srcLength, byte[] dst, int dstLength) - { - return ZstandardDecoder.TryDecompress ( - src.AsSpan (0, srcLength), - dst.AsSpan (0, dstLength), - out int bytesWritten) ? bytesWritten : -1; - } - static readonly ArrayPool bytePool = ArrayPool.Shared; static int Usage () @@ -56,7 +46,10 @@ static bool UncompressDLL (Stream inputStream, string fileName, string filePath, reader.Read (sourceBytes, 0, inputLength); byte[] assemblyBytes = bytePool.Rent ((int)decompressedLength); - int decoded = ZstdDecompress (sourceBytes, inputLength, assemblyBytes, (int)decompressedLength); + int decoded = ZstandardDecoder.TryDecompress ( + sourceBytes.AsSpan (0, inputLength), + assemblyBytes.AsSpan (0, (int)decompressedLength), + out int bytesWritten) ? bytesWritten : -1; if (decoded != (int)decompressedLength) { Console.Error.WriteLine ($" Failed to decompress Zstd data of {fileName} (decoded: {decoded})"); retVal = false; From 78d6a04a9d6fa163cb4168ff0d34d45e634190f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:08:38 +0000 Subject: [PATCH 7/9] Address review feedback for Zstd compression changes Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Utilities/AssemblyCompression.cs | 18 ++++- .../Utilities/Zstd.cs | 72 +++++++++++++++---- .../Xamarin.Android.Common.targets | 4 +- tools/tmt/ApkManagedTypeResolver.cs | 48 +++++++++---- 4 files changed, 112 insertions(+), 30 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs b/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs index f2d6b7ce4ce..da0b4704ef7 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs @@ -74,10 +74,24 @@ static CompressionResult Compress (AssemblyData data, string outputFilePath) int fileSize = checked((int)fi.Length); sourceBytes = bytePool.Rent (fileSize); using (var fs = File.Open (data.SourcePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { - bytesRead = fs.Read (sourceBytes, 0, fileSize); + bytesRead = 0; + while (bytesRead < fileSize) { + int read = fs.Read (sourceBytes, bytesRead, fileSize - bytesRead); + if (read == 0) + break; + + bytesRead += read; + } } - destBytes = bytePool.Rent (Zstd.MaximumOutputSize (bytesRead)); + if (bytesRead != fileSize) + return CompressionResult.EncodingFailed; + + int maxOutputSize = Zstd.MaximumOutputSize (bytesRead); + if (maxOutputSize <= 0) + return CompressionResult.EncodingFailed; + + destBytes = bytePool.Rent (maxOutputSize); int encodedLength = Zstd.Compress (sourceBytes, bytesRead, destBytes); if (encodedLength < 0) return CompressionResult.EncodingFailed; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs b/src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs index bd81780dc69..9491f2553ef 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs @@ -17,34 +17,49 @@ static class Zstd // ZSTD_cParameter value for ZSTD_c_compressionLevel (see zstd.h). const int ZSTD_c_compressionLevel = 100; + const int ZstdCompressionLevel = 3; - [DllImport (ZstdLibrary)] + [DllImport (ZstdLibrary, CallingConvention = CallingConvention.Cdecl)] static extern UIntPtr ZSTD_compressBound (UIntPtr srcSize); - [DllImport (ZstdLibrary)] + [DllImport (ZstdLibrary, CallingConvention = CallingConvention.Cdecl)] static extern IntPtr ZSTD_createCCtx (); - [DllImport (ZstdLibrary)] + [DllImport (ZstdLibrary, CallingConvention = CallingConvention.Cdecl)] static extern UIntPtr ZSTD_freeCCtx (IntPtr cctx); - [DllImport (ZstdLibrary)] + [DllImport (ZstdLibrary, CallingConvention = CallingConvention.Cdecl)] static extern UIntPtr ZSTD_CCtx_setParameter (IntPtr cctx, int param, int value); - [DllImport (ZstdLibrary)] + [DllImport (ZstdLibrary, CallingConvention = CallingConvention.Cdecl)] static extern UIntPtr ZSTD_compress2 (IntPtr cctx, byte[] dst, UIntPtr dstCapacity, byte[] src, UIntPtr srcSize); - [DllImport (ZstdLibrary)] + [DllImport (ZstdLibrary, CallingConvention = CallingConvention.Cdecl)] static extern uint ZSTD_isError (UIntPtr code); - [DllImport (ZstdLibrary)] - static extern int ZSTD_maxCLevel (); - /// /// Returns the maximum size that compressed data of bytes can occupy. /// public static int MaximumOutputSize (int inputSize) { - return checked ((int) (ulong) ZSTD_compressBound ((UIntPtr) (uint) inputSize)); + if (inputSize < 0) + return -1; + + try { + UIntPtr result = ZSTD_compressBound ((UIntPtr) (uint) inputSize); + if (ZSTD_isError (result) != 0) + return -1; + + ulong maxOutputSize = (ulong) result; + if (maxOutputSize > int.MaxValue) + return -1; + + return (int) maxOutputSize; + } catch (DllNotFoundException) { + return -1; + } catch (EntryPointNotFoundException) { + return -1; + } } /// @@ -54,20 +69,49 @@ public static int MaximumOutputSize (int inputSize) /// public static int Compress (byte[] input, int inputLength, byte[] output) { - IntPtr cctx = ZSTD_createCCtx (); + if (input == null) + throw new ArgumentNullException (nameof (input)); + if (output == null) + throw new ArgumentNullException (nameof (output)); + if (inputLength < 0 || inputLength > input.Length) + throw new ArgumentOutOfRangeException (nameof (inputLength)); + + IntPtr cctx; + try { + cctx = ZSTD_createCCtx (); + } catch (DllNotFoundException) { + return -1; + } catch (EntryPointNotFoundException) { + return -1; + } + if (cctx == IntPtr.Zero) return -1; try { - ZSTD_CCtx_setParameter (cctx, ZSTD_c_compressionLevel, ZSTD_maxCLevel ()); + UIntPtr setParameterResult = ZSTD_CCtx_setParameter (cctx, ZSTD_c_compressionLevel, ZstdCompressionLevel); + if (ZSTD_isError (setParameterResult) != 0) + return -1; UIntPtr result = ZSTD_compress2 (cctx, output, (UIntPtr) (uint) output.Length, input, (UIntPtr) (uint) inputLength); if (ZSTD_isError (result) != 0) return -1; - return checked ((int) (ulong) result); + ulong encodedLength = (ulong) result; + if (encodedLength > int.MaxValue) + return -1; + + return (int) encodedLength; + } catch (DllNotFoundException) { + return -1; + } catch (EntryPointNotFoundException) { + return -1; } finally { - ZSTD_freeCCtx (cctx); + try { + ZSTD_freeCCtx (cctx); + } catch (DllNotFoundException) { + } catch (EntryPointNotFoundException) { + } } } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 0af5e923d83..47d1a76d490 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2165,8 +2165,8 @@ because xbuild doesn't support framework reference assemblies. Date: Fri, 26 Jun 2026 16:41:52 +0200 Subject: [PATCH 8/9] Update CoreCLR apkdesc baselines from CI build 1482888 The size-regression baselines committed for the Zstd switch were generated in a different environment and did not match the artifacts produced by CI, causing BuildReleaseArm64 (CoreCLR) to fail the apkdiff regression check. Replace the Simple and XForms CoreCLR .apkdesc files with the descriptors produced by the failing CI run (build 1482888). Compared to the LZ4 baselines on main, Zstd still shrinks libassembly-store.so (Simple: 3,461,344 -> 2,903,016; XForms: 14,137,920 -> 11,658,648); the previous committed values were simply too optimistic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc | 6 +++--- .../Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc index 4ff6b6b482a..9cd4d7cbfcb 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc @@ -8,7 +8,7 @@ "Size": 402352 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 2482600 + "Size": 2903016 }, "lib/arm64-v8a/libclrjit.so": { "Size": 2824392 @@ -32,7 +32,7 @@ "Size": 168080 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 20776 + "Size": 20984 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -59,5 +59,5 @@ "Size": 1904 } }, - "PackageSize": 7026107 + "PackageSize": 7452091 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc index d63fb8ef4c2..042ee6a45db 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc @@ -32,7 +32,7 @@ "Size": 2396 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 9622600 + "Size": 11658648 }, "lib/arm64-v8a/libclrjit.so": { "Size": 2824392 @@ -56,7 +56,7 @@ "Size": 168080 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 147616 + "Size": 147824 }, "META-INF/androidx.activity_activity.version": { "Size": 6 @@ -2234,5 +2234,5 @@ "Size": 794696 } }, - "PackageSize": 18730573 + "PackageSize": 20786765 } \ No newline at end of file From d9a8b0c6550659003d16243e131202d96bf0197e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 16:52:50 +0200 Subject: [PATCH 9/9] Fix Zstd.Compress doc to match actual compression level The XML doc said compression used "the maximum compression level," but Compress sets ZstdCompressionLevel = 3, which is zstd's default level (not its maximum, 22). This was a leftover from an earlier ZSTD_maxCLevel () version. Update the doc to describe the actual behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs b/src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs index 9491f2553ef..775c66fff6e 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs @@ -64,7 +64,7 @@ public static int MaximumOutputSize (int inputSize) /// /// Compresses bytes from into - /// using the maximum compression level. Returns the number of + /// using zstd's default compression level. Returns the number of /// bytes written to , or -1 if compression failed. /// public static int Compress (byte[] input, int inputLength, byte[] output)