diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9668219..59cc3b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ jobs: - uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x 7.0.x - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1be9010..af3a9b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,6 @@ jobs: - uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x 7.0.x - name: Pack run: dotnet pack -c Release -o pkg diff --git a/bench/LibDeflate.Benchmarks/CustomAllocatorBenchmarks.cs b/bench/LibDeflate.Benchmarks/CustomAllocatorBenchmarks.cs new file mode 100644 index 0000000..26466c2 --- /dev/null +++ b/bench/LibDeflate.Benchmarks/CustomAllocatorBenchmarks.cs @@ -0,0 +1,57 @@ +using BenchmarkDotNet.Attributes; +using LibDeflate.Imports; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LibDeflate.Benchmarks; + +[MemoryDiagnoser] +[SimpleJob] +public class CustomAllocatorBenchmarks +{ + [GlobalSetup(Target = nameof(CompressorAllocCustom))] + public void SetCustomAllocator() + { + Console.WriteLine("Custom Allocator: set"); + CustomMemoryAllocator.libdeflate_set_memory_allocator(malloc, free); + + static nint malloc(nuint len) => Marshal.AllocHGlobal((nint)len); + + static void free(nint alloc) => Marshal.FreeHGlobal(alloc); + } + + [GlobalSetup(Target = nameof(CompressorAllocCustomUnsafe))] + public unsafe void SetCustomAllocatorUnsafe() + { + Console.WriteLine("Custom Unsafe Allocator: set"); + CustomMemoryAllocator.libdeflate_set_memory_allocator_unsafe(&malloc_unsafe, &free_unsafe); + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + static unsafe void* malloc_unsafe(nuint len) => NativeMemory.Alloc(len); + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + static unsafe void free_unsafe(void* alloc) => NativeMemory.Free(alloc); + } + + [Benchmark(Baseline = true)] + public void CompressorAlloc() + { + var compressor = Compression.libdeflate_alloc_compressor(0); + Compression.libdeflate_free_compressor(compressor); + } + + [Benchmark] + public void CompressorAllocCustom() + { + var compressor = Compression.libdeflate_alloc_compressor(0); + Compression.libdeflate_free_compressor(compressor); + } + + [Benchmark] + public void CompressorAllocCustomUnsafe() + { + var compressor = Compression.libdeflate_alloc_compressor(0); + Compression.libdeflate_free_compressor(compressor); + } +} diff --git a/bench/LibDeflate.Benchmarks/DeflateCompressorBenchmarks.cs b/bench/LibDeflate.Benchmarks/DeflateCompressorBenchmarks.cs index 957c13d..b47dcd3 100644 --- a/bench/LibDeflate.Benchmarks/DeflateCompressorBenchmarks.cs +++ b/bench/LibDeflate.Benchmarks/DeflateCompressorBenchmarks.cs @@ -4,6 +4,7 @@ using System.IO; using System.IO.Compression; using System.Linq; +using System.Runtime.CompilerServices; namespace LibDeflate.Benchmarks; @@ -11,49 +12,90 @@ namespace LibDeflate.Benchmarks; [SimpleJob] public class DeflateCompressorBenchmarks { - public static IEnumerable Inputs => from key in TestFiles.Keys - from level in Levels - select new object[] { key, level }; + private static string AssetBase + { + get + { + var cwd = Directory.GetCurrentDirectory(); - private static Dictionary TestFiles { get; } = Directory.EnumerateFiles(@"texts/") - .Where(fn => !Path.GetExtension(fn).Equals(".gz", StringComparison.OrdinalIgnoreCase)) - .ToDictionary(fn => Path.GetFileNameWithoutExtension(fn), File.ReadAllBytes); + string assetsDir; + while (!Directory.Exists(assetsDir = Path.Join(cwd, "assets"))) + { + cwd = Path.GetDirectoryName(cwd); + } + + return assetsDir; + } + } private static IEnumerable Levels { get { - //yield return 0; + yield return 0; yield return 1; - //yield return 6; - //yield return 9; + yield return -1; + yield return 9; } } - [Benchmark(Baseline = true)] - [ArgumentsSource(nameof(Inputs))] - public void DeflateSIO(string testFile, int level) + [GlobalSetup] + public static void PrepareTestAssets() { - var compressionLevel = level switch + var assetsFolder = Path.Join(AssetBase, "UncompressedTestFiles"); + var testFiles = new Dictionary(); + foreach (var file in Directory.EnumerateFiles(assetsFolder, null, SearchOption.AllDirectories)) { - 0 => CompressionLevel.NoCompression, - 1 => CompressionLevel.Fastest, - 6 => CompressionLevel.Optimal, - 9 => CompressionLevel.SmallestSize - }; + var key = Path.GetRelativePath(assetsFolder, file); + testFiles.Add(key, File.ReadAllBytes(file)); + } + + TestFiles = testFiles; + } + + public static Dictionary TestFiles { get; set; } + public static IEnumerable GetInputs() => from key in TestFiles.Keys + from level in Levels + select new object[] { key, level }; + +#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static CompressionLevel ToLevelEnum(int level) => level switch + { + 0 => CompressionLevel.NoCompression, + 1 => CompressionLevel.Fastest, + -1 => CompressionLevel.Optimal, + 9 => CompressionLevel.SmallestSize + }; +#pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). + + [Benchmark(Baseline = true)] + [ArgumentsSource(nameof(GetInputs))] + public void DeflateSIO(string testFile, int level) + { var input = TestFiles[testFile]; using var outputMs = new MemoryStream(input.Length); - using var deflateStream = new DeflateStream(outputMs, compressionLevel); + using var deflateStream = new DeflateStream(outputMs, ToLevelEnum(level)); deflateStream.Write(input); } [Benchmark] - [ArgumentsSource(nameof(Inputs))] - public void DeflateLibdeflate(string testFile, int level) + [ArgumentsSource(nameof(GetInputs))] + public void DeflateLibdeflate_MemoryOwner(string testFile, int level) { var input = TestFiles[testFile]; using var compressor = new DeflateCompressor(level); using var owner = compressor.Compress(input); } + + [Benchmark] + [ArgumentsSource(nameof(GetInputs))] + public void DeflateLibdeflate_Buffer(string testFile, int level) + { + var input = TestFiles[testFile]; + using var compressor = new DeflateCompressor(level); + var output = new byte[input.Length]; + var bytesWritten = compressor.Compress(input, output); + } } diff --git a/bench/LibDeflate.Benchmarks/LibDeflate.Benchmarks.csproj b/bench/LibDeflate.Benchmarks/LibDeflate.Benchmarks.csproj index aa69a7d..5c710c1 100644 --- a/bench/LibDeflate.Benchmarks/LibDeflate.Benchmarks.csproj +++ b/bench/LibDeflate.Benchmarks/LibDeflate.Benchmarks.csproj @@ -2,12 +2,13 @@ Exe - net6.0 + net7.0 false + true - + diff --git a/bench/LibDeflate.Benchmarks/Program.cs b/bench/LibDeflate.Benchmarks/Program.cs index e1c0549..af5ba4f 100644 --- a/bench/LibDeflate.Benchmarks/Program.cs +++ b/bench/LibDeflate.Benchmarks/Program.cs @@ -1,8 +1,3 @@ using BenchmarkDotNet.Running; -namespace LibDeflate.Benchmarks; - -public class Program -{ - public static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); -} +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file diff --git a/src/LibDeflate/Checksums/Adler32.cs b/src/LibDeflate/Checksums/Adler32.cs index f19661c..f03f260 100644 --- a/src/LibDeflate/Checksums/Adler32.cs +++ b/src/LibDeflate/Checksums/Adler32.cs @@ -7,25 +7,14 @@ namespace LibDeflate.Checksums; public struct Adler32 { - private bool _initialized; - //because we have to supply 1 as the initial value - //we must always retrieve this through Hash private uint _currentAdler; - public uint Hash - { - get - { - if (!_initialized) - { - _currentAdler = 1; - _initialized = true; - } - return _currentAdler; - } - } + + public Adler32() => _currentAdler = 1; + + public readonly uint Hash => _currentAdler; public void Append(ReadOnlySpan input) - => _currentAdler = AppendCore(Hash, input); + => _currentAdler = AppendCore(_currentAdler, input); public uint Compute(ReadOnlySpan input) => _currentAdler = AppendCore(1, input); diff --git a/src/LibDeflate/Checksums/Crc32.cs b/src/LibDeflate/Checksums/Crc32.cs index 1ca2da3..41e0b0b 100644 --- a/src/LibDeflate/Checksums/Crc32.cs +++ b/src/LibDeflate/Checksums/Crc32.cs @@ -9,7 +9,7 @@ public struct Crc32 { private uint _currentCrc; - public uint Hash => _currentCrc; + public readonly uint Hash => _currentCrc; public void Append(ReadOnlySpan input) => _currentCrc = AppendCore(_currentCrc, input); diff --git a/src/LibDeflate/Compressor.cs b/src/LibDeflate/Compressor.cs index 57bdedd..abf1f19 100644 --- a/src/LibDeflate/Compressor.cs +++ b/src/LibDeflate/Compressor.cs @@ -51,7 +51,7 @@ protected Compressor(int compressionLevel) return null; } - return output.Slice(0, (int)bytesWritten); + return output[..(int)bytesWritten]; } catch { diff --git a/src/LibDeflate/Imports/Checksums.cs b/src/LibDeflate/Imports/Checksums.cs index d09dfef..4117d71 100644 --- a/src/LibDeflate/Imports/Checksums.cs +++ b/src/LibDeflate/Imports/Checksums.cs @@ -1,11 +1,11 @@ -using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace LibDeflate.Imports; -using size_t = System.UIntPtr; +using size_t = nuint; -internal static class Checksums +internal static partial class Checksums { /// /// libdeflate_adler32() updates a running Adler-32 checksum with 'len' bytes of @@ -13,8 +13,9 @@ internal static class Checksums /// required initial value for 'adler' is 1. This value is also returned when /// 'buffer' is specified as NULL. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern UInt32 libdeflate_adler32(UInt32 adler, in byte buffer, size_t len); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial uint libdeflate_adler32(uint adler, in byte buffer, size_t len); /// /// libdeflate_crc32() updates a running CRC-32 checksum with 'len' bytes of data @@ -22,6 +23,7 @@ internal static class Checksums /// initial value for 'crc' is 0. This value is also returned when 'buffer' is /// specified as NULL. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern UInt32 libdeflate_crc32(UInt32 crc, in byte buffer, size_t len); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial uint libdeflate_crc32(uint crc, in byte buffer, size_t len); } diff --git a/src/LibDeflate/Imports/Compression.cs b/src/LibDeflate/Imports/Compression.cs index 094b143..ed9baff 100644 --- a/src/LibDeflate/Imports/Compression.cs +++ b/src/LibDeflate/Imports/Compression.cs @@ -1,11 +1,12 @@ -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace LibDeflate.Imports; -using libdeflate_compressor = System.IntPtr; -using size_t = System.UIntPtr; +using libdeflate_compressor = nint; +using size_t = nuint; -internal static class Compression +internal static partial class Compression { /// /// libdeflate_alloc_compressor() allocates a new compressor that supports @@ -25,8 +26,16 @@ internal static class Compression /// A single compressor is not safe to use by multiple threads concurrently. /// However, different threads may use different compressors concurrently. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern libdeflate_compressor libdeflate_alloc_compressor(int compression_level); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial libdeflate_compressor libdeflate_alloc_compressor(int compression_level); + + /// + /// Like but allows specifying advanced options per-compressor. + /// + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial libdeflate_compressor libdeflate_alloc_compressor_ex(int compression_level, in libdeflate_options options); /// /// libdeflate_deflate_compress() performs raw DEFLATE compression on a buffer of @@ -35,9 +44,9 @@ internal static class Compression /// bytes. The return value is the compressed size in bytes, or 0 if the data /// could not be compressed to 'out_nbytes_avail' bytes or fewer. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern size_t libdeflate_deflate_compress(libdeflate_compressor compressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail); - + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial size_t libdeflate_deflate_compress(libdeflate_compressor compressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail); /// /// libdeflate_deflate_compress_bound() returns a worst-case upper bound on the @@ -64,44 +73,50 @@ internal static class Compression /// libdeflate_deflate_compress() returns 0, indicating that the compressed data /// did not fit into the provided output buffer. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern size_t libdeflate_deflate_compress_bound(libdeflate_compressor compressor, size_t in_nbytes); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial size_t libdeflate_deflate_compress_bound(libdeflate_compressor compressor, size_t in_nbytes); /// /// Like libdeflate_deflate_compress(), but stores the data in the zlib wrapper /// format. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern size_t libdeflate_zlib_compress(libdeflate_compressor compressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial size_t libdeflate_zlib_compress(libdeflate_compressor compressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail); /// /// Like libdeflate_deflate_compress_bound(), but assumes the data will be /// compressed with libdeflate_zlib_compress() rather than with /// libdeflate_deflate_compress(). /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern size_t libdeflate_zlib_compress_bound(libdeflate_compressor compressor, size_t in_nbytes); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial size_t libdeflate_zlib_compress_bound(libdeflate_compressor compressor, size_t in_nbytes); /// /// Like libdeflate_deflate_compress(), but stores the data in the gzip wrapper /// format. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern size_t libdeflate_gzip_compress(libdeflate_compressor compressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial size_t libdeflate_gzip_compress(libdeflate_compressor compressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail); /// /// Like libdeflate_deflate_compress_bound(), but assumes the data will be /// compressed with libdeflate_gzip_compress() rather than with /// libdeflate_deflate_compress(). /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern size_t libdeflate_gzip_compress_bound(libdeflate_compressor compressor, size_t in_nbytes); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial size_t libdeflate_gzip_compress_bound(libdeflate_compressor compressor, size_t in_nbytes); /// /// libdeflate_free_compressor() frees a compressor that was allocated with /// libdeflate_alloc_compressor(). If a NULL pointer is passed in, no action is /// taken. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern void libdeflate_free_compressor(libdeflate_compressor compressor); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial void libdeflate_free_compressor(libdeflate_compressor compressor); } diff --git a/src/LibDeflate/Imports/Constants.cs b/src/LibDeflate/Imports/Constants.cs index 5986130..4dfe053 100644 --- a/src/LibDeflate/Imports/Constants.cs +++ b/src/LibDeflate/Imports/Constants.cs @@ -2,6 +2,9 @@ [assembly: InternalsVisibleTo($"{nameof(LibDeflate)}.Tests")] [assembly: InternalsVisibleTo($"{nameof(LibDeflate)}.DangerousTests")] +[assembly: InternalsVisibleTo($"{nameof(LibDeflate)}.Benchmarks")] +[assembly: DisableRuntimeMarshalling] + namespace LibDeflate.Imports; internal static class Constants diff --git a/src/LibDeflate/Imports/CustomMemoryAllocator.cs b/src/LibDeflate/Imports/CustomMemoryAllocator.cs index 7549b85..b6825ba 100644 --- a/src/LibDeflate/Imports/CustomMemoryAllocator.cs +++ b/src/LibDeflate/Imports/CustomMemoryAllocator.cs @@ -1,16 +1,17 @@ using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace LibDeflate.Imports; -using size_t = UIntPtr; +using size_t = nuint; -internal static class CustomMemoryAllocator +internal static partial class CustomMemoryAllocator { - //[UnmanagedFunctionPointer(CallingConvention.Cdecl)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate IntPtr malloc_func(size_t size); - //[UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void free_func(IntPtr alloc); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void free_func(nint alloc); /// /// Install a custom memory allocator which libdeflate will use for all memory @@ -20,6 +21,19 @@ internal static class CustomMemoryAllocator /// There must not be any libdeflate_compressor or libdeflate_decompressor /// structures in existence when calling this function. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern void libdeflate_set_memory_allocator(malloc_func malloc, free_func free); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial void libdeflate_set_memory_allocator(malloc_func malloc, free_func free); + + /// + /// Install a custom memory allocator which libdeflate will use for all memory + /// allocations. 'malloc_func' is a function that must behave like malloc(), and + /// 'free_func' is a function that must behave like free(). + /// + /// There must not be any libdeflate_compressor or libdeflate_decompressor + /// structures in existence when calling this function. + /// + [LibraryImport(Constants.DllName, EntryPoint = "libdeflate_set_memory_allocator")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static unsafe partial void libdeflate_set_memory_allocator_unsafe(delegate* unmanaged[Cdecl] malloc, delegate* unmanaged[Cdecl] free); } diff --git a/src/LibDeflate/Imports/Decompression.cs b/src/LibDeflate/Imports/Decompression.cs index f32a4e3..cfd1551 100644 --- a/src/LibDeflate/Imports/Decompression.cs +++ b/src/LibDeflate/Imports/Decompression.cs @@ -1,11 +1,12 @@ -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace LibDeflate.Imports; -using libdeflate_decompressor = System.IntPtr; -using size_t = System.UIntPtr; +using libdeflate_decompressor = nint; +using size_t = nuint; -internal static class Decompression +internal static partial class Decompression { /// /// Result of a call to libdeflate_deflate_decompress(), @@ -48,8 +49,16 @@ public enum libdeflate_result /// A single decompressor is not safe to use by multiple threads concurrently. /// However, different threads may use different decompressors concurrently. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern libdeflate_decompressor libdeflate_alloc_decompressor(); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial libdeflate_decompressor libdeflate_alloc_decompressor(); + + /// + /// Like but allows specifying advanced options per-decompressor. + /// + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial libdeflate_decompressor libdeflate_alloc_decompressor_ex(in libdeflate_options options); /// /// libdeflate_deflate_decompress() decompresses the DEFLATE-compressed stream @@ -83,8 +92,9 @@ public enum libdeflate_result /// not large enough but no other problems were encountered, or another /// nonzero result code if decompression failed for another reason. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern libdeflate_result libdeflate_deflate_decompress(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_out_nbytes_ret); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial libdeflate_result libdeflate_deflate_decompress(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_out_nbytes_ret); /// /// Like libdeflate_deflate_decompress(), but adds the 'actual_in_nbytes_ret' @@ -92,8 +102,9 @@ public enum libdeflate_result /// then the actual compressed size of the DEFLATE stream (aligned to the next /// byte boundary) is written to *actual_in_nbytes_ret. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern libdeflate_result libdeflate_deflate_decompress_ex(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_in_nbytes_ret, out size_t actual_out_nbytes_ret); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial libdeflate_result libdeflate_deflate_decompress_ex(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_in_nbytes_ret, out size_t actual_out_nbytes_ret); /// /// Like libdeflate_deflate_decompress(), but assumes the zlib wrapper format @@ -103,8 +114,9 @@ public enum libdeflate_result /// than 'in_nbytes'. If you need to know exactly where the zlib stream ended, /// use libdeflate_zlib_decompress_ex(). /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern libdeflate_result libdeflate_zlib_decompress(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_out_nbytes_ret); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial libdeflate_result libdeflate_zlib_decompress(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_out_nbytes_ret); /// /// Like libdeflate_deflate_decompress(), but assumes the zlib wrapper format @@ -114,8 +126,9 @@ public enum libdeflate_result /// than 'in_nbytes'. If you need to know exactly where the zlib stream ended, /// use libdeflate_zlib_decompress_ex(). /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern libdeflate_result libdeflate_zlib_decompress_ex(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_in_nbytes_ret, out size_t actual_out_nbytes_ret); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial libdeflate_result libdeflate_zlib_decompress_ex(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_in_nbytes_ret, out size_t actual_out_nbytes_ret); /// /// Like libdeflate_deflate_decompress(), but assumes the gzip wrapper format @@ -125,8 +138,9 @@ public enum libdeflate_result /// will be decompressed. Use libdeflate_gzip_decompress_ex() if you need /// multi-member support. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern libdeflate_result libdeflate_gzip_decompress(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_out_nbytes_ret); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial libdeflate_result libdeflate_gzip_decompress(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_out_nbytes_ret); /// /// Like libdeflate_gzip_decompress(), but adds the 'actual_in_nbytes_ret' @@ -135,14 +149,16 @@ public enum libdeflate_result /// buffer was decompressed), then the actual number of input bytes consumed is /// written to *actual_in_nbytes_ret. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern libdeflate_result libdeflate_gzip_decompress_ex(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_in_nbytes_ret, out size_t actual_out_nbytes_ret); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial libdeflate_result libdeflate_gzip_decompress_ex(libdeflate_decompressor decompressor, in byte @in, size_t in_nbytes, ref byte @out, size_t out_nbytes_avail, out size_t actual_in_nbytes_ret, out size_t actual_out_nbytes_ret); /// /// libdeflate_free_decompressor() frees a decompressor that was allocated with /// libdeflate_alloc_decompressor(). If a NULL pointer is passed in, no action /// is taken. /// - [DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)] - public static extern void libdeflate_free_decompressor(libdeflate_decompressor compressor); + [LibraryImport(Constants.DllName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial void libdeflate_free_decompressor(libdeflate_decompressor compressor); } \ No newline at end of file diff --git a/src/LibDeflate/Imports/libdeflate_options.cs b/src/LibDeflate/Imports/libdeflate_options.cs new file mode 100644 index 0000000..52c8dd9 --- /dev/null +++ b/src/LibDeflate/Imports/libdeflate_options.cs @@ -0,0 +1,56 @@ +namespace LibDeflate.Imports; + +using size_t = nuint; + +/// +/// Advanced options. This is the options structure that +/// +/// and +/// require. Most users won't need this and should just use the non-"_ex" +/// functions instead. +/// +internal readonly unsafe struct libdeflate_options(delegate* unmanaged[Cdecl] malloc, delegate* unmanaged[Cdecl] free)//(size_t sizeof_options, malloc_func malloc, free_func free) +{ + /// + /// This field must be set to the struct size. This field exists for + /// extensibility, so that fields can be appended to this struct in + /// future versions of libdeflate while still supporting old binaries. + /// + public readonly size_t sizeof_options = (size_t)sizeof(libdeflate_options); + + /// + /// An optional custom memory allocator to use for this (de)compressor. + /// 'malloc_func' must be a function that behaves like malloc(). + /// + /// + /// This is useful in cases where a process might have multiple users of + /// libdeflate who want to use different memory allocators. For example, + /// a library might want to use libdeflate with a custom memory allocator + /// without interfering with user code that might use libdeflate too. + /// + /// This takes priority over the "global" memory allocator (which by + /// default is malloc() and free(), but can be changed by + /// libdeflate_set_memory_allocator()). Moreover, libdeflate will never + /// call the "global" memory allocator if a per-(de)compressor custom + /// allocator is always given. + /// + public readonly delegate* unmanaged[Cdecl] malloc = malloc; + + /// + /// An optional custom memory deallocator to use for this (de)compressor. + /// 'free_func' must be a function that behaves like free(). + /// + /// + /// This is useful in cases where a process might have multiple users of + /// libdeflate who want to use different memory allocators. For example, + /// a library might want to use libdeflate with a custom memory allocator + /// without interfering with user code that might use libdeflate too. + /// + /// This takes priority over the "global" memory allocator (which by + /// default is malloc() and free(), but can be changed by + /// libdeflate_set_memory_allocator()). Moreover, libdeflate will never + /// call the "global" memory allocator if a per-(de)compressor custom + /// allocator is always given. + /// + public readonly delegate* unmanaged[Cdecl] free = free; +} \ No newline at end of file diff --git a/src/LibDeflate/LibDeflate.csproj b/src/LibDeflate/LibDeflate.csproj index 4c51334..f7c617f 100644 --- a/src/LibDeflate/LibDeflate.csproj +++ b/src/LibDeflate/LibDeflate.csproj @@ -1,24 +1,30 @@  - net6.0;netstandard2.0 + net7.0 LibDeflate.NET - 1.18.0 + 1.19.0 jzebedee MIT https://github.com/jzebedee/LibDeflate libdeflate.net libdeflate deflate zlib gzip gz crc32 crc adler32 adler LibDeflate.NET is a managed wrapper around libdeflate, a native library for fast, whole-buffer DEFLATE-based compression and decompression. - latest + preview enable - true true true true snupkg + true + + + + + + true @@ -33,7 +39,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -45,7 +51,7 @@ - + diff --git a/test/LibDeflate.DangerousTests/CustomMemoryAllocatorTests.cs b/test/LibDeflate.DangerousTests/CustomMemoryAllocatorTests.cs index 655810f..b71f8bc 100644 --- a/test/LibDeflate.DangerousTests/CustomMemoryAllocatorTests.cs +++ b/test/LibDeflate.DangerousTests/CustomMemoryAllocatorTests.cs @@ -1,4 +1,6 @@ using LibDeflate.Imports; +using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Xunit; @@ -9,14 +11,21 @@ public class CustomMemoryAllocatorTests private static int mallocCount = 0; private static int freeCount = 0; - //[UnmanagedCallersOnly] + //This is not thread-safe, so we disable parallel tests in xunit.runner.json + private static void VerifyAndResetCount() + { + (mallocCount, freeCount) = (0, 0); + + Assert.Equal(0, mallocCount); + Assert.Equal(0, freeCount); + } + private static nint malloc(nuint len) { mallocCount++; return Marshal.AllocHGlobal((nint)len); } - //[UnmanagedCallersOnly] private static void free(nint alloc) { freeCount++; @@ -24,10 +33,108 @@ private static void free(nint alloc) } [Fact] - public void UseCustomAllocatorsTest() + public void UseGlobalCustomAllocatorsTest() { + VerifyAndResetCount(); + CustomMemoryAllocator.libdeflate_set_memory_allocator(malloc, free); + //test compressor + { + //allocate something + var compressor = Compression.libdeflate_alloc_compressor(0); + Assert.Equal(1, mallocCount); + + //free something + Compression.libdeflate_free_compressor(compressor); + Assert.Equal(1, freeCount); + } + + //test decompressor + { + var decompressor = Decompression.libdeflate_alloc_decompressor(); + Assert.Equal(2, mallocCount); + + Decompression.libdeflate_free_decompressor(decompressor); + Assert.Equal(2, freeCount); + } + } + + [Fact] + public unsafe void UsePerCompressorCustomAllocatorsTest() + { + int startingGlobalMallocs = mallocCount; + int startingGlobalFrees = freeCount; + + int localMallocs = 0; + int localFrees = 0; + + CustomMemoryAllocator.malloc_func malloc; + malloc = (nuint len) => + { + localMallocs++; + return Marshal.AllocHGlobal((nint)len); + }; + + CustomMemoryAllocator.free_func free; + free = (nint alloc) => + { + localFrees++; + Marshal.FreeHGlobal(alloc); + }; + + var options = new libdeflate_options( + (delegate* unmanaged[Cdecl])Marshal.GetFunctionPointerForDelegate(malloc), + (delegate* unmanaged[Cdecl])Marshal.GetFunctionPointerForDelegate(free) + ); + + //test compressor + { + var compressor = Compression.libdeflate_alloc_compressor_ex(0, options); + Assert.Equal(1, localMallocs); + Assert.Equal(startingGlobalMallocs, mallocCount); + + Compression.libdeflate_free_compressor(compressor); + Assert.Equal(1, localFrees); + Assert.Equal(startingGlobalFrees, freeCount); + } + + //test decompressor + { + var decompressor = Decompression.libdeflate_alloc_decompressor_ex(options); + Assert.Equal(2, localMallocs); + Assert.Equal(startingGlobalMallocs, mallocCount); + + Decompression.libdeflate_free_decompressor(decompressor); + Assert.Equal(2, localFrees); + Assert.Equal(startingGlobalFrees, freeCount); + } + + GC.KeepAlive(malloc); + GC.KeepAlive(free); + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static unsafe void* malloc_unsafe(nuint len) + { + mallocCount++; + return NativeMemory.Alloc(len); + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static unsafe void free_unsafe(void* alloc) + { + freeCount++; + NativeMemory.Free(alloc); + } + + [Fact] + public unsafe void UseCustomAllocatorsUnsafeTest() + { + VerifyAndResetCount(); + + CustomMemoryAllocator.libdeflate_set_memory_allocator_unsafe(&malloc_unsafe, &free_unsafe); + //allocate something var compressor = Compression.libdeflate_alloc_compressor(0); Assert.Equal(1, mallocCount); diff --git a/test/LibDeflate.DangerousTests/LibDeflate.DangerousTests.csproj b/test/LibDeflate.DangerousTests/LibDeflate.DangerousTests.csproj index 099dc8f..670df7b 100644 --- a/test/LibDeflate.DangerousTests/LibDeflate.DangerousTests.csproj +++ b/test/LibDeflate.DangerousTests/LibDeflate.DangerousTests.csproj @@ -1,14 +1,13 @@  - net6.0;net7.0 + net7.0 false - + latest - - - + True + @@ -17,13 +16,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/LibDeflate.Tests/ImportTests/CompressionTests.cs b/test/LibDeflate.Tests/ImportTests/CompressionTests.cs index 83c2875..f520590 100644 --- a/test/LibDeflate.Tests/ImportTests/CompressionTests.cs +++ b/test/LibDeflate.Tests/ImportTests/CompressionTests.cs @@ -21,7 +21,7 @@ public void AllocAndFreeCompressorTest(int compressionLevel) var compressor = Imports.Compression.libdeflate_alloc_compressor(compressionLevel); try { - Assert.NotEqual(compressor, IntPtr.Zero); + Assert.NotEqual(0, compressor); } finally { @@ -41,7 +41,7 @@ public void DeflateCompressTest(int compressionLevel) Span outputBuffer = stackalloc byte[512]; var numBytesCompressed = Imports.Compression.libdeflate_deflate_compress(compressor, MemoryMarshal.GetReference(testBytes), (UIntPtr)testBytes.Length, ref MemoryMarshal.GetReference(outputBuffer), (UIntPtr)outputBuffer.Length); - var compressedBuffer = outputBuffer.Slice(0, (int)numBytesCompressed); + var compressedBuffer = outputBuffer[..(int)numBytesCompressed]; var actual = Encoding.UTF8.GetString(FlateToBuffer(compressedBuffer, CompressionMode.Decompress).Span); Assert.Equal(expected, actual); } @@ -74,7 +74,7 @@ public void ZlibCompressTest(int compressionLevel) Span outputBuffer = stackalloc byte[512]; var numBytesCompressed = Imports.Compression.libdeflate_zlib_compress(compressor, MemoryMarshal.GetReference(testBytes), (UIntPtr)testBytes.Length, ref MemoryMarshal.GetReference(outputBuffer), (UIntPtr)outputBuffer.Length); - var compressedBuffer = outputBuffer.Slice(0, (int)numBytesCompressed); + var compressedBuffer = outputBuffer[..(int)numBytesCompressed]; var actual = Encoding.UTF8.GetString(ZlibToBuffer(compressedBuffer, CompressionMode.Decompress).Span); Assert.Equal(expected, actual); } @@ -105,7 +105,7 @@ public void GzipCompressTest(int compressionLevel) Span outputBuffer = stackalloc byte[512]; var numBytesCompressed = Imports.Compression.libdeflate_gzip_compress(compressor, MemoryMarshal.GetReference(testBytes), (UIntPtr)testBytes.Length, ref MemoryMarshal.GetReference(outputBuffer), (UIntPtr)outputBuffer.Length); - var compressedBuffer = outputBuffer.Slice(0, (int)numBytesCompressed); + var compressedBuffer = outputBuffer[..(int)numBytesCompressed]; var actual = Encoding.UTF8.GetString(GzipToBuffer(compressedBuffer, CompressionMode.Decompress).Span); Assert.Equal(expected, actual); } diff --git a/test/LibDeflate.Tests/Lib/SixLabors.ZlibStream.dll b/test/LibDeflate.Tests/Lib/SixLabors.ZlibStream.dll index 13dae90..3b62201 100644 Binary files a/test/LibDeflate.Tests/Lib/SixLabors.ZlibStream.dll and b/test/LibDeflate.Tests/Lib/SixLabors.ZlibStream.dll differ diff --git a/test/LibDeflate.Tests/LibDeflate.Tests.csproj b/test/LibDeflate.Tests/LibDeflate.Tests.csproj index 5adb742..ddc701d 100644 --- a/test/LibDeflate.Tests/LibDeflate.Tests.csproj +++ b/test/LibDeflate.Tests/LibDeflate.Tests.csproj @@ -1,20 +1,23 @@  + + + - net6.0;net7.0 + net7.0 false latest - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all