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 23728b0..5c710c1 100644 --- a/bench/LibDeflate.Benchmarks/LibDeflate.Benchmarks.csproj +++ b/bench/LibDeflate.Benchmarks/LibDeflate.Benchmarks.csproj @@ -2,8 +2,9 @@ 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/Imports/Checksums.cs b/src/LibDeflate/Imports/Checksums.cs index 816383f..4117d71 100644 --- a/src/LibDeflate/Imports/Checksums.cs +++ b/src/LibDeflate/Imports/Checksums.cs @@ -1,10 +1,11 @@ -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace LibDeflate.Imports; 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 @@ -12,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 = Constants.CallConv, ExactSpelling = true)] - public static extern uint libdeflate_adler32(uint 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 @@ -21,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 = Constants.CallConv, ExactSpelling = true)] - public static extern uint libdeflate_crc32(uint 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 53ea83b..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 = nint; using size_t = nuint; -internal static class Compression +internal static partial class Compression { /// /// libdeflate_alloc_compressor() allocates a new compressor that supports @@ -25,14 +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 = Constants.CallConv, ExactSpelling = true)] - 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. /// - [DllImport(Constants.DllName, CallingConvention = Constants.CallConv, ExactSpelling = true)] - public static extern libdeflate_compressor libdeflate_alloc_compressor_ex(int compression_level, in libdeflate_options options); + [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 @@ -41,8 +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 = Constants.CallConv, ExactSpelling = true)] - 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 @@ -69,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 = Constants.CallConv, ExactSpelling = true)] - 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 = Constants.CallConv, ExactSpelling = true)] - 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 = Constants.CallConv, ExactSpelling = true)] - 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 = Constants.CallConv, ExactSpelling = true)] - 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 = Constants.CallConv, ExactSpelling = true)] - 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 = Constants.CallConv, ExactSpelling = true)] - 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 907d10b..4dfe053 100644 --- a/src/LibDeflate/Imports/Constants.cs +++ b/src/LibDeflate/Imports/Constants.cs @@ -1,12 +1,13 @@ using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; [assembly: InternalsVisibleTo($"{nameof(LibDeflate)}.Tests")] [assembly: InternalsVisibleTo($"{nameof(LibDeflate)}.DangerousTests")] +[assembly: InternalsVisibleTo($"{nameof(LibDeflate)}.Benchmarks")] +[assembly: DisableRuntimeMarshalling] + namespace LibDeflate.Imports; internal static class Constants { public const string DllName = "libdeflate"; - public const CallingConvention CallConv = CallingConvention.Cdecl; } diff --git a/src/LibDeflate/Imports/CustomMemoryAllocator.cs b/src/LibDeflate/Imports/CustomMemoryAllocator.cs index e48b9a2..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 = Constants.CallConv, ExactSpelling = true)] - public static extern void libdeflate_set_memory_allocator(malloc_func malloc, free_func free); -} \ No newline at end of file + [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 bd3f380..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,14 +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 = Constants.CallConv, ExactSpelling = true)] - 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. /// - [DllImport(Constants.DllName, CallingConvention = Constants.CallConv, ExactSpelling = true)] - public static extern libdeflate_decompressor libdeflate_alloc_decompressor_ex(in libdeflate_options options); + [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 @@ -89,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 = Constants.CallConv, ExactSpelling = true)] - 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' @@ -98,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 = Constants.CallConv, ExactSpelling = true)] - 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 @@ -109,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 = Constants.CallConv, ExactSpelling = true)] - 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 @@ -120,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 = Constants.CallConv, ExactSpelling = true)] - 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 @@ -131,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 = Constants.CallConv, ExactSpelling = true)] - 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' @@ -141,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 = Constants.CallConv, ExactSpelling = true)] - 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 = Constants.CallConv, ExactSpelling = true)] - public static extern void libdeflate_free_decompressor(libdeflate_decompressor decompressor); + [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 index 8367689..52c8dd9 100644 --- a/src/LibDeflate/Imports/libdeflate_options.cs +++ b/src/LibDeflate/Imports/libdeflate_options.cs @@ -1,10 +1,6 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -namespace LibDeflate.Imports; +namespace LibDeflate.Imports; -using static LibDeflate.Imports.CustomMemoryAllocator; -using size_t = UIntPtr; +using size_t = nuint; /// /// Advanced options. This is the options structure that @@ -13,44 +9,14 @@ namespace LibDeflate.Imports; /// require. Most users won't need this and should just use the non-"_ex" /// functions instead. /// -internal readonly struct libdeflate_options +internal readonly unsafe struct libdeflate_options(delegate* unmanaged[Cdecl] malloc, delegate* unmanaged[Cdecl] free)//(size_t sizeof_options, malloc_func malloc, free_func free) { - private static readonly size_t Size = (nuint)(nint)Unsafe.SizeOf(); - - public libdeflate_options(malloc_func malloc, free_func free) - { -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(malloc); - ArgumentNullException.ThrowIfNull(free); -#else - ThrowIfNull(malloc); - ThrowIfNull(free); -#endif - - this.sizeof_options = Size; - this.malloc = malloc; - this.free = free; - -#if !NET6_0_OR_GREATER - static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) - { - if(argument is null) - { - ThrowHelperArgumentNull(paramName!); - } - - [DoesNotReturn] - static void ThrowHelperArgumentNull(string paramName) => throw new ArgumentNullException(paramName); - } -#endif - } - /// /// 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; + public readonly size_t sizeof_options = (size_t)sizeof(libdeflate_options); /// /// An optional custom memory allocator to use for this (de)compressor. @@ -68,7 +34,7 @@ static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(na /// call the "global" memory allocator if a per-(de)compressor custom /// allocator is always given. /// - public readonly malloc_func malloc; + public readonly delegate* unmanaged[Cdecl] malloc = malloc; /// /// An optional custom memory deallocator to use for this (de)compressor. @@ -86,5 +52,5 @@ static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(na /// call the "global" memory allocator if a per-(de)compressor custom /// allocator is always given. /// - public readonly free_func free; -} + 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 815a692..f7c617f 100644 --- a/src/LibDeflate/LibDeflate.csproj +++ b/src/LibDeflate/LibDeflate.csproj @@ -1,7 +1,7 @@  - net6.0;netstandard2.0 + net7.0 LibDeflate.NET 1.19.0 jzebedee @@ -18,6 +18,7 @@ true true snupkg + true diff --git a/test/LibDeflate.DangerousTests/CustomMemoryAllocatorTests.cs b/test/LibDeflate.DangerousTests/CustomMemoryAllocatorTests.cs index 2f2f2d8..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++; @@ -26,6 +35,8 @@ private static void free(nint alloc) [Fact] public void UseGlobalCustomAllocatorsTest() { + VerifyAndResetCount(); + CustomMemoryAllocator.libdeflate_set_memory_allocator(malloc, free); //test compressor @@ -50,22 +61,32 @@ public void UseGlobalCustomAllocatorsTest() } [Fact] - public void UsePerCompressorCustomAllocatorsTest() + public unsafe void UsePerCompressorCustomAllocatorsTest() { int startingGlobalMallocs = mallocCount; int startingGlobalFrees = freeCount; int localMallocs = 0; int localFrees = 0; - var options = new libdeflate_options((nuint len) => + + CustomMemoryAllocator.malloc_func malloc; + malloc = (nuint len) => { localMallocs++; return Marshal.AllocHGlobal((nint)len); - }, (nint alloc) => + }; + + 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 { @@ -88,5 +109,38 @@ public void UsePerCompressorCustomAllocatorsTest() 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); + + //free something + Compression.libdeflate_free_compressor(compressor); + Assert.Equal(1, freeCount); } } diff --git a/test/LibDeflate.DangerousTests/LibDeflate.DangerousTests.csproj b/test/LibDeflate.DangerousTests/LibDeflate.DangerousTests.csproj index 7fb0d87..670df7b 100644 --- a/test/LibDeflate.DangerousTests/LibDeflate.DangerousTests.csproj +++ b/test/LibDeflate.DangerousTests/LibDeflate.DangerousTests.csproj @@ -1,10 +1,12 @@  - net6.0;net7.0 + net7.0 false latest + + True diff --git a/test/LibDeflate.Tests/LibDeflate.Tests.csproj b/test/LibDeflate.Tests/LibDeflate.Tests.csproj index 80c11b0..ddc701d 100644 --- a/test/LibDeflate.Tests/LibDeflate.Tests.csproj +++ b/test/LibDeflate.Tests/LibDeflate.Tests.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0 + net7.0 false latest