adamhathcock / sharpcompress

SharpCompress is a fully managed C# library to deal with many compression types and formats.
MIT License
2.22k stars 476 forks source link

Some functions of sharpcompress don't work when Reflection is disabled in NativeAOT #826

Closed klimatr26 closed 2 months ago

klimatr26 commented 2 months ago

Hello.

I have been trying to include sharpcompress in a NativeAOT application. It normally works fine.

However, when I disable Reflection via <IlcDisableReflection>true</IlcDisableReflection>, some parts stop working and throw and exception related to Reflection being disabled.

I used the "ZipArchive with Writing API Example" from the wiki for one program with Reflection disabled and it seemed to work fine. However, using other code like the "Reader (forward-only streams) API Example" example throws an exception when Reflection is disabled (but works fine when Reflection is enabled in NativeAOT).

I know this only affects a very small number of users, since JIT always should support Reflection, and NativeAOT supports enough Reflection features to use sharpcompress without issue. However, disabling Reflection reduces the size of the resulting executable significantly, and that's why I was trying to use that mode.

Have a good day.

adamhathcock commented 2 months ago

I'm happy to rework this to support this scenario, I just don't know what is needed. Is it just a matter of putting that in the csproj? Because it builds for me.

klimatr26 commented 2 months ago

Hello.

The <IlcDisableReflection>true</IlcDisableReflection> switch is enabled by the programmer in the csproj of their program, if they want to publish with AOT and disable reflection.

I got this exception when trying to list the contents of a ZIP file from a Task:

Unhandled Exception: MT140698580042400: TypeInitialization_Type_NoTypeAvailable
 ---> MT140698580039448: Reflection_Disabled
   at Internal.Reflection.RuntimeTypeInfo.get_Assembly() + 0x6f
   at System.Text.BaseCodePageEncoding.GetEncodingDataStream(String) + 0x1d
   at System.Text.BaseCodePageEncoding..cctor() + 0x5d
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0xb9
   Exception_EndOfInnerExceptionStack
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0x14b
   at System.Runtime.CompilerServices.ClassConstructorRunner.CheckStaticClassConstructionReturnGCStaticBase(StaticClassConstructionContext*, Object) + 0xd
   at System.Text.BaseCodePageEncoding.GetCodePageByteSize(Int32) + 0x120
   at System.Text.CodePagesEncodingProvider.GetEncoding(Int32) + 0x8b
   at System.Text.CodePagesEncodingProvider.GetEncoding(Int32) + 0x1ed
   at System.Text.EncodingProvider.GetEncodingFromProvider(Int32) + 0x32
   at System.Text.Encoding.GetEncoding(Int32) + 0x11
   at System.Text.EncodingHelper.GetSupportedConsoleEncoding(Int32) + 0xf
   at System.Console.get_OutputEncoding() + 0x64
   at System.ConsolePal.OpenStandardOutput() + 0x9
   at System.Console.<get_Out>g__EnsureInitialized|26_0() + 0x37
   at System.Console.WriteLine(String) + 0xd
   at MyProgram.Program.<Main>d__0.MoveNext() + 0x4e5
--- End of stack trace from previous location ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() + 0x20
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task) + 0x62
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task, ConfigureAwaitOptions) + 0x4b
   at MyProgram.Program.<Main>(String[]) + 0x29
   at MyProgram!<BaseAddress>+0x1af5f0

Does this mean the problem actually exists in System.Text?

Also, the code for listing the contents of the ZIP file is inside a try-catch block, so... ???????. BUT ALSO, the program was working without Reflection until I added SharpCompress, and the rest of the program still works correctly without Reflection (except for the ZIP listing part).

However, I don't understand why a similar program that creates 2 zip archives (one Deflate and one LZMA), lists the contents of one of them, and extracts one of them, works correctly with Reflection disabled.

I'm not sure if it is related to being run from a Task, because I remember that I got a similar Exception when I tried to list and extract the contents of a 7-zip archive -which cannot be opened in Windows 11 (maybe they forgot to enable LZMA in libarchive?)- which normally can be extracted fine by SharpCompress.

UPDATE: I tried the previously problematic code for extracting the 7z archive, and it worked fine without Reflection.

At this point, I'm REALLY confused. Maybe this isn't related to SharpCompress? It is weird that it only happens in (async) code that calls SharpCompress. I saw that you already replaced Activator calls with delegates.

Unrelated: Does SharpCompress load the entire solid 7z block to memory? Memory usage increases beyond the sliding window when I extract solid entries. 7zdec from the LZMA SDK also does this. Also unrelated: SharpCompress marks the minimum version for extracting LZMA entries of a ZIP file as 20 instead of 63. The Writer code doesn't seem to have any checks for this.

Have a good day.

adamhathcock commented 2 months ago

there's a lot to unpack here

there could be a reference leak with 7z/LZMA but don't know, haven't tested. I don't load everything into memory other than that the file has to be random access.

LZMA minimum version is also something I haven't looked into

klimatr26 commented 2 months ago

Hello.

I did more testing, and I have some code.

The following code throws an Unhandled Exception even inside a try-catch:

using SharpCompress.Archives.Zip;

namespace AsyncList
{
    internal class Program
    {
        //static async Task Main(string[] args)
        static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                Console.WriteLine("Provide a file to list its contents.");
            }
            else
            {
                try
                {
                    //Console.WriteLine("Try to create Reader");
                    var reader = new Reader(args[0]);
                    //string result = await reader.Read();
                    //string result = await Task.Run(() => reader.Read());
                    //Console.WriteLine("Try to read contents");
                    string result = reader.Read();
                    if (result.Length != 0)
                    {
                        Console.WriteLine(result);
                        Environment.Exit(1);
                    }
                    //var files = await Task.Run(() => reader.List());
                    //Console.WriteLine("Try to create list of contents");
                    string[] files = reader.List();
                    //Console.WriteLine("Try to list contents to console");
                    foreach (var file in files)
                    {
                        Console.WriteLine(file);
                    }
                }
                catch (Exception ex) //Still throws an Unhandled Exception
                {
                    Console.WriteLine(ex.ToString());
                }
            }
        }
    }

    internal class Reader(string filePath)
    {
        public string FilePath { get; } = filePath;

        //public string Read(CancellationToken? cancellationToken = null, IProgress<string>? fileNameProgress = null, IProgress<int>? progress = null)
        public string Read()
        {
            string result = string.Empty;
            try
            {
                using var archive = ZipArchive.Open(FilePath);
            }
            catch (Exception e)
            {
                result = e.Message;
            }
            return result;
        }

        public string[] List()
        {
            string[] result = Array.Empty<string>();
            try
            {
                using (var archive = ZipArchive.Open(FilePath))
                {
                    int noOfEntries = archive.Entries.Count;
                    result = new string[noOfEntries];
                    var i = 0;
                    foreach (var entry in archive.Entries)
                    {
                        result[i] = entry.Key;
                        i++;
                    }
                }
            }
            catch { }
            return result;
        }
    }
}

However, simply removing the // from Console.WriteLine("Try to create Reader"); seems to solve the issue. (Maybe should close the issue? I still don't understand why this happens specifically with SharpCompress) The program then behaves normally.

This is REALLY weird to me, and I don't think SharpCompress is the problem here.

My csproj contains

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>true</InvariantGlobalization>
    <IlcDisableReflection>true</IlcDisableReflection>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="SharpCompress" Version="0.36.0" />
  </ItemGroup>

</Project>

Sorry to bother you with this.

Have a good day.