diff --git a/src/typst_core/src/lib.rs b/src/typst_core/src/lib.rs index 071f61e..410f7f2 100644 --- a/src/typst_core/src/lib.rs +++ b/src/typst_core/src/lib.rs @@ -87,18 +87,7 @@ impl Default for CompileResult { } } -/// Creates a compiler that reads its document either from `input_path` or from `input_source`. -/// -/// # Safety -/// -/// `root`, `input_path`, `package_path` and `sys_inputs` must be null or NUL-terminated strings, -/// `font_paths` must be null or point to `font_paths_len` such strings, and `input_source` must be -/// null or point to `input_source_len` bytes. Unlike the others, the source is passed with an -/// explicit length and may contain NUL bytes. All of them need only stay valid for the duration of -/// the call. The returned compiler is owned by the caller and must be released with -/// [`free_compiler`]. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn create_compiler( +unsafe fn create_compiler_inner( root: *const c_char, input_path: *const c_char, input_source: *const u8, @@ -139,12 +128,18 @@ pub unsafe extern "C" fn create_compiler( // which means an empty document. let input_content = if input_source.is_null() { None + } else if input_source_len == 0 { + Some(String::new()) } else { let bytes = unsafe { std::slice::from_raw_parts(input_source, input_source_len) }; Some(String::from_utf8_lossy(bytes).into_owned()) }; - let sys_inputs_str = unsafe { CStr::from_ptr(sys_inputs).to_str().unwrap_or("{}") }; + let sys_inputs_str = if sys_inputs.is_null() { + "{}" + } else { + unsafe { CStr::from_ptr(sys_inputs).to_str().unwrap_or("{}") } + }; let font_paths_vec: Vec = unsafe { let slice: &[*const c_char] = if font_paths.is_null() || font_paths_len == 0 { @@ -155,7 +150,12 @@ pub unsafe extern "C" fn create_compiler( slice .iter() - .map(|&p| PathBuf::from(CStr::from_ptr(p).to_str().unwrap_or(""))) + .copied() + .filter(|p| !p.is_null()) + .filter_map(|p| { + let s = CStr::from_ptr(p).to_str().unwrap_or(""); + (!s.is_empty()).then(|| PathBuf::from(s)) + }) .collect() }; @@ -187,6 +187,46 @@ pub unsafe extern "C" fn create_compiler( } } +/// Creates a compiler that reads its document either from `input_path` or from `input_source`. +/// +/// # Safety +/// +/// `root`, `input_path`, `package_path` and `sys_inputs` must be null or NUL-terminated strings, +/// `font_paths` must be null or point to `font_paths_len` such strings (each of which may also be +/// null), and `input_source` must be null or point to `input_source_len` bytes. Unlike the others, +/// the source is passed with an explicit length and may contain NUL bytes. All of them need only +/// stay valid for the duration of the call. The returned compiler is owned by the caller and must +/// be released with [`free_compiler`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn create_compiler( + root: *const c_char, + input_path: *const c_char, + input_source: *const u8, + input_source_len: usize, + font_paths: *const *const c_char, + font_paths_len: usize, + package_path: *const c_char, + sys_inputs: *const c_char, + ignore_system_fonts: bool, + ignore_system_packages: bool, +) -> *mut Compiler { + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe { + create_compiler_inner( + root, + input_path, + input_source, + input_source_len, + font_paths, + font_paths_len, + package_path, + sys_inputs, + ignore_system_fonts, + ignore_system_packages, + ) + })) + .unwrap_or(ptr::null_mut()) +} + /// Releases a compiler created by [`create_compiler`]. A null pointer is ignored. /// /// Results previously returned by [`compile`] are unaffected: they own their memory and stay valid. @@ -197,11 +237,13 @@ pub unsafe extern "C" fn create_compiler( /// freed, and no other thread may be using it. #[unsafe(no_mangle)] pub unsafe extern "C" fn free_compiler(compiler: *mut Compiler) { - if !compiler.is_null() { - unsafe { - let _ = Box::from_raw(compiler); + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + if !compiler.is_null() { + unsafe { + let _ = Box::from_raw(compiler); + } } - } + })); } /// Replaces the `sys.inputs` dictionary the next compilation will see. Returns `false` if the @@ -213,26 +255,26 @@ pub unsafe extern "C" fn free_compiler(compiler: *mut Compiler) { /// null or a NUL-terminated JSON object that stays valid for the duration of the call. #[unsafe(no_mangle)] pub unsafe extern "C" fn set_sys_inputs(compiler: *mut Compiler, sys_inputs: *const c_char) -> bool { - if compiler.is_null() { - return false; - } - let compiler = unsafe { &mut *compiler }; + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + if compiler.is_null() { + return false; + } + let compiler = unsafe { &mut *compiler }; - let sys_inputs_str = if sys_inputs.is_null() { - "{}" - } else { - unsafe { CStr::from_ptr(sys_inputs).to_str().unwrap_or("{}") } - }; + let sys_inputs_str = if sys_inputs.is_null() { + "{}" + } else { + unsafe { CStr::from_ptr(sys_inputs).to_str().unwrap_or("{}") } + }; - let inputs: Dict = match serde_json::from_str(sys_inputs_str) { - Ok(d) => d, - Err(_) => return false, - }; + let inputs: Dict = match serde_json::from_str(sys_inputs_str) { + Ok(d) => d, + Err(_) => return false, + }; - match compiler.0.set_inputs(inputs) { - Ok(_) => true, - Err(_) => false, - } + compiler.0.set_inputs(inputs).is_ok() + })) + .unwrap_or(false) } fn compile_inner( @@ -488,7 +530,7 @@ pub unsafe extern "C" fn compile( /// result frees the same allocations twice. #[unsafe(no_mangle)] pub unsafe extern "C" fn free_compile_result(result: CompileResult) { - unsafe { + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe { if !result.buffers.is_null() { let buffers = Box::from_raw(std::ptr::slice_from_raw_parts_mut( result.buffers, @@ -510,7 +552,7 @@ pub unsafe extern "C" fn free_compile_result(result: CompileResult) { if !result.error_ptr.is_null() { free_raw_slice(result.error_ptr, result.error_len); } - } + })); } /// How many evictions a cached result survives without being used, when the cache is trimmed after @@ -523,7 +565,7 @@ const DEFAULT_MAX_CACHE_AGE: usize = 10; /// still holding. #[unsafe(no_mangle)] pub extern "C" fn reset_world() { - comemo::evict(DEFAULT_MAX_CACHE_AGE); + let _ = std::panic::catch_unwind(|| comemo::evict(DEFAULT_MAX_CACHE_AGE)); } /// Clears the process-global incremental compilation cache completely. diff --git a/src/typst_core/tests/compile.rs b/src/typst_core/tests/compile.rs index 9c444c8..ef6a3df 100644 --- a/src/typst_core/tests/compile.rs +++ b/src/typst_core/tests/compile.rs @@ -197,4 +197,31 @@ fn the_cache_can_be_cleared_while_other_threads_compile() { } assert!(evictions > 0, "no eviction overlapped the compilations"); +} + +#[test] +fn create_compiler_accepts_null_sys_inputs_and_null_font_path_entries() { + let source = b"= Null pointer resilience"; + let font_paths: [*const c_char; 2] = [std::ptr::null(), std::ptr::null()]; + + let compiler = unsafe { + create_compiler( + std::ptr::null(), + std::ptr::null(), + source.as_ptr(), + source.len(), + font_paths.as_ptr(), + font_paths.len(), + std::ptr::null(), + std::ptr::null(), + true, + true, + ) + }; + assert!(!compiler.is_null(), "failed to create compiler with null sys_inputs and font_paths"); + + let pdf = compile_to_pdf(compiler); + assert!(pdf.starts_with(b"%PDF-")); + + unsafe { free_compiler(compiler) }; } \ No newline at end of file diff --git a/src/typstsharp.tests/Tests.cs b/src/typstsharp.tests/Tests.cs index 64393e2..0b915d0 100644 --- a/src/typstsharp.tests/Tests.cs +++ b/src/typstsharp.tests/Tests.cs @@ -1410,6 +1410,98 @@ public async Task WarningsFromACleanCompileAreEmptyAndStillImmutable() await Assert.That(document.Warnings.Count).IsEqualTo(0); } + [Test] + public async Task FontPathsWithNullOrEmptyEntriesAreIgnoredSafely() + { + using var compiler = TypstCompiler.FromSource( + "= Null font path entry", + fonts: new Fonts(IncludeSystemFonts: false, FontPaths: [null!, ""])); + + var plainText = GetPlainText(compiler.CompilePdf()); + await Assert.That(plainText).Contains("Null font path entry"); + } + + [Test] + public async Task DisposingCompilerConcurrentlyIsSafe() + { + var compiler = TypstCompiler.FromSource("= Concurrent dispose"); + using var barrier = new Barrier(8); + + var tasks = Enumerable.Range(0, 8).Select(_ => Task.Run(() => + { + barrier.SignalAndWait(); + compiler.Dispose(); + })).ToArray(); + + await Task.WhenAll(tasks); + + await Assert.That(() => compiler.CompilePdf()).Throws(); + await Assert.That(() => compiler.SetSysInputs(new Dictionary())).Throws(); + } + + /// + /// If a document is disposed while an asynchronous write still has the native buffer pinned + /// (for instance when a cancellation token unwinds a caller's using scope while OS overlapped + /// I/O is still in flight), freeing must wait until the pin and lease are released. + /// + [Test] + public async Task DisposingDocumentWhileBufferIsPinnedDefersFreeUntilUnpinned() + { + using var compiler = TypstCompiler.FromSource("= Pinned across disposal\n\n#lorem(100)"); + var document = compiler.CompileToDocument(); + var expected = document.GetOutputBytes(); + + var pinnedSignal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var disposedSignal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var sink = new PinningAsyncStream(pinnedSignal, disposedSignal); + + var writeTask = document.CopyOutputToAsync(sink).AsTask(); + + await pinnedSignal.Task; + + // Dispose the document while the stream still holds a MemoryHandle pin over its native buffer. + document.Dispose(); + + // New operations must immediately observe that the document is disposed. + await Assert.That(() => document.GetOutputBytes()).Throws(); + + // Let the in-flight pinned write read from the pointer and then unpin. + disposedSignal.SetResult(); + var captured = await writeTask.ContinueWith(_ => sink.CapturedBytes); + + await Assert.That(captured.SequenceEqual(expected)).IsTrue(); + } + + private sealed class PinningAsyncStream( + TaskCompletionSource pinnedSignal, + TaskCompletionSource disposedSignal) : Stream + { + public byte[] CapturedBytes { get; private set; } = []; + + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + using var handle = buffer.Pin(); + pinnedSignal.SetResult(); + await disposedSignal.Task.ConfigureAwait(false); + + CapturedBytes = CopyFromHandle(handle, buffer.Length); + } + + private static unsafe byte[] CopyFromHandle(MemoryHandle handle, int length) => + new ReadOnlySpan(handle.Pointer, length).ToArray(); + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } + private const string TwoPageSource = """ First page #pagebreak() diff --git a/src/typstsharp.tests/typstsharp.tests.csproj b/src/typstsharp.tests/typstsharp.tests.csproj index d1d604f..c938276 100644 --- a/src/typstsharp.tests/typstsharp.tests.csproj +++ b/src/typstsharp.tests/typstsharp.tests.csproj @@ -3,6 +3,7 @@ enable enable + true Exe net10.0 true diff --git a/src/typstsharp/Bindings.g.cs b/src/typstsharp/Bindings.g.cs index d455655..2c5cf70 100644 --- a/src/typstsharp/Bindings.g.cs +++ b/src/typstsharp/Bindings.g.cs @@ -24,11 +24,11 @@ internal static unsafe partial class NativeMethods /// # Safety /// /// `root`, `input_path`, `package_path` and `sys_inputs` must be null or NUL-terminated strings, - /// `font_paths` must be null or point to `font_paths_len` such strings, and `input_source` must be - /// null or point to `input_source_len` bytes. Unlike the others, the source is passed with an - /// explicit length and may contain NUL bytes. All of them need only stay valid for the duration of - /// the call. The returned compiler is owned by the caller and must be released with - /// [`free_compiler`]. + /// `font_paths` must be null or point to `font_paths_len` such strings (each of which may also be + /// null), and `input_source` must be null or point to `input_source_len` bytes. Unlike the others, + /// the source is passed with an explicit length and may contain NUL bytes. All of them need only + /// stay valid for the duration of the call. The returned compiler is owned by the caller and must + /// be released with [`free_compiler`]. /// [DllImport(__DllName, EntryPoint = "create_compiler", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern Compiler* create_compiler(byte* root, byte* input_path, byte* input_source, nuint input_source_len, byte** font_paths, nuint font_paths_len, byte* package_path, byte* sys_inputs, [MarshalAs(UnmanagedType.U1)] bool ignore_system_fonts, [MarshalAs(UnmanagedType.U1)] bool ignore_system_packages); diff --git a/src/typstsharp/NativeBuffers.cs b/src/typstsharp/NativeBuffers.cs index fd1975f..7453313 100644 --- a/src/typstsharp/NativeBuffers.cs +++ b/src/typstsharp/NativeBuffers.cs @@ -17,9 +17,11 @@ internal sealed unsafe class NativeBufferMemoryManager : MemoryManager private readonly TypstDocument _owner; private readonly byte* _pointer; private readonly int _length; + private int _disposed; public NativeBufferMemoryManager(TypstDocument owner, byte* pointer, int length) { + owner.AcquireLease(); _owner = owner; _pointer = pointer; _length = length; @@ -27,34 +29,46 @@ public NativeBufferMemoryManager(TypstDocument owner, byte* pointer, int length) public override Span GetSpan() { - ObjectDisposedException.ThrowIf(_owner.IsDisposed, _owner); + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); return new Span(_pointer, _length); } /// - /// Native memory is never relocated by the GC, so pinning is a no-op and only has to hand back - /// the address of the requested element. One past the end is allowed, matching the semantics of - /// over an array. + /// Acquires a lease on the owning for the lifetime of the returned + /// so that disposing the document while an asynchronous I/O operation + /// still has the buffer pinned defers freeing the native memory until runs. /// public override MemoryHandle Pin(int elementIndex = 0) { - ObjectDisposedException.ThrowIf(_owner.IsDisposed, _owner); + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); + _owner.AcquireLease(); + try + { + if ((uint)elementIndex > (uint)_length) + { + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + } - if ((uint)elementIndex > (uint)_length) + return new MemoryHandle(_pointer + elementIndex, default, this); + } + catch { - throw new ArgumentOutOfRangeException(nameof(elementIndex)); + _owner.ReleaseLease(); + throw; } - - return new MemoryHandle(_pointer + elementIndex); } public override void Unpin() { + _owner.ReleaseLease(); } protected override void Dispose(bool disposing) { - GC.KeepAlive(_owner); + if (Interlocked.Exchange(ref _disposed, 1) == 0) + { + _owner.ReleaseLease(); + } } } diff --git a/src/typstsharp/TypstCompiler.cs b/src/typstsharp/TypstCompiler.cs index 8e2b7f0..2eb071d 100644 --- a/src/typstsharp/TypstCompiler.cs +++ b/src/typstsharp/TypstCompiler.cs @@ -14,7 +14,7 @@ public class TypstCompiler : IDisposable { public static string EmptyDictionaryJson => "{}"; private unsafe CsBindgen.Compiler* _compiler; - private bool _disposed = false; + private int _disposed; private static readonly JsonSerializerOptions sourceGenOptions = new() { TypeInfoResolver = SourceGenerationContext.Default @@ -698,7 +698,7 @@ public void Compile(string outputFile, string format, float ppi = 144.0f, IEnume /// Thrown if the inputs fail to be set in the native compiler. public unsafe void SetSysInputs(IDictionary inputs) { - if (_disposed) throw new ObjectDisposedException(nameof(TypstCompiler)); + EnsureNotDisposed(); var sysInputsJson = JsonSerializer.Serialize>(inputs, sourceGenOptions); var sysInputsPtr = Marshal.StringToCoTaskMemUTF8(sysInputsJson); @@ -727,18 +727,17 @@ public void Dispose() protected virtual unsafe void Dispose(bool disposing) { - if (_disposed) + if (Interlocked.Exchange(ref _disposed, 1) != 0) { return; } - if (_compiler != null) + var compiler = _compiler; + _compiler = null; + if (compiler != null) { - CsBindgen.NativeMethods.free_compiler(_compiler); - _compiler = null; + CsBindgen.NativeMethods.free_compiler(compiler); } - - _disposed = true; } ~TypstCompiler() @@ -748,10 +747,7 @@ protected virtual unsafe void Dispose(bool disposing) private void EnsureNotDisposed() { - if (_disposed) - { - throw new ObjectDisposedException(nameof(TypstCompiler)); - } + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); } } diff --git a/src/typstsharp/TypstDocument.cs b/src/typstsharp/TypstDocument.cs index a006c4a..e48fba5 100644 --- a/src/typstsharp/TypstDocument.cs +++ b/src/typstsharp/TypstDocument.cs @@ -32,11 +32,18 @@ namespace typstsharp; /// public sealed class TypstDocument : IDisposable { + // Bit 0 records whether Dispose or the finalizer has run. + // Bits 1..30 hold the active reference count: 1 for the document's own lifetime, plus 1 for + // each in-flight read, copy, NativeBufferMemoryManager, or pinned MemoryHandle. Native memory + // is freed only when Dispose has been requested AND the reference count reaches zero. + private const int DisposedBit = 1; + private const int OneRef = 2; + private unsafe CsBindgen.CompileResult _native; private readonly int _outputCount; private readonly IReadOnlyList _warnings; private readonly long _nativeByteCount; - private int _disposed; + private int _state = OneRef; internal unsafe TypstDocument(CsBindgen.CompileResult native) { @@ -95,7 +102,45 @@ internal unsafe TypstDocument(CsBindgen.CompileResult native) /// Warnings reported by the Typst compiler. public IReadOnlyList Warnings => _warnings; - internal bool IsDisposed => Volatile.Read(ref _disposed) != 0; + internal bool IsDisposed => (Volatile.Read(ref _state) & DisposedBit) != 0; + + internal Lease EnterLease() + { + AcquireLease(); + return new Lease(this); + } + + internal void AcquireLease() + { + while (true) + { + int current = Volatile.Read(ref _state); + ObjectDisposedException.ThrowIf((current & DisposedBit) != 0, this); + + if (current > int.MaxValue - OneRef) + { + throw new InvalidOperationException("Maximum concurrent lease count exceeded."); + } + + if (Interlocked.CompareExchange(ref _state, current + OneRef, current) == current) + { + return; + } + } + } + + internal void ReleaseLease() + { + if (Interlocked.Add(ref _state, -OneRef) == DisposedBit) + { + FreeCore(); + } + } + + internal readonly struct Lease(TypstDocument document) : IDisposable + { + public void Dispose() => document.ReleaseLease(); + } /// /// The length in bytes of one output buffer, without touching its content. Useful for setting a @@ -104,9 +149,8 @@ internal unsafe TypstDocument(CsBindgen.CompileResult native) /// The zero-based buffer index; defaults to the only buffer of a PDF. public unsafe long GetOutputLength(int output = 0) { - long length = (long)GetBuffer(output).len; - KeepAlive(); - return length; + using var _ = EnterLease(); + return (long)GetBuffer(output).len; } /// @@ -133,6 +177,7 @@ public unsafe ReadOnlySpan GetOutputSpan(int output = 0) /// The zero-based buffer index; defaults to the only buffer of a PDF. public unsafe Stream OpenOutputStream(int output = 0) { + using var _ = EnterLease(); var buffer = GetBuffer(output); return new OutputStream(this, buffer.ptr, (long)buffer.len); } @@ -145,8 +190,8 @@ public unsafe Stream OpenOutputStream(int output = 0) public unsafe void CopyOutputTo(Stream destination, int output = 0) { ArgumentNullException.ThrowIfNull(destination); + using var _ = EnterLease(); destination.Write(GetOutputSpan(output)); - KeepAlive(); } /// @@ -177,13 +222,12 @@ public async ValueTask CopyOutputToAsync(Stream destination, int output = 0, Can /// The zero-based buffer index; defaults to the only buffer of a PDF. public unsafe void WriteOutputToFile(string path, int output = 0) { + using var _ = EnterLease(); var content = GetOutputSpan(output); using (var handle = OpenFile(path, content.Length, FileOptions.None)) { RandomAccess.Write(handle, content, fileOffset: 0); } - - KeepAlive(); } /// @@ -213,10 +257,10 @@ public async Task WriteOutputToFileAsync(string path, int output = 0, Cancellati /// public unsafe IMemoryOwner RentOutput(int output = 0) { + using var _ = EnterLease(); var content = GetOutputSpan(output); var owner = new PooledBuffer(content.Length); content.CopyTo(owner.Span); - KeepAlive(); return owner; } @@ -227,9 +271,8 @@ public unsafe IMemoryOwner RentOutput(int output = 0) /// The zero-based buffer index; defaults to the only buffer of a PDF. public unsafe byte[] GetOutputBytes(int output = 0) { - var bytes = GetOutputSpan(output).ToArray(); - KeepAlive(); - return bytes; + using var _ = EnterLease(); + return GetOutputSpan(output).ToArray(); } private static SafeFileHandle OpenFile(string path, long length, FileOptions options) => @@ -249,6 +292,7 @@ private static SafeFileHandle OpenFile(string path, long length, FileOptions opt /// private unsafe NativeBufferMemoryManager CreateBufferManager(int output) { + using var _ = EnterLease(); var buffer = GetBuffer(output); return new NativeBufferMemoryManager(this, buffer.ptr, ToLength(buffer.len)); } @@ -302,17 +346,36 @@ public void Dispose() Free(); } - private unsafe void Free() + private void Free() { - // Exchange rather than a plain bool check: a double free corrupts the native heap, which is a - // far worse outcome than the torn reads the rest of the type tolerates. - if (Interlocked.Exchange(ref _disposed, 1) != 0) + // Mark the document disposed and drop its own lifetime reference. If any lease or pinned + // MemoryHandle is still active, the last ReleaseLease() will run FreeCore() when it finishes. + while (true) { - return; + int current = Volatile.Read(ref _state); + if ((current & DisposedBit) != 0) + { + return; + } + + int next = (current - OneRef) | DisposedBit; + if (Interlocked.CompareExchange(ref _state, next, current) == current) + { + if (next == DisposedBit) + { + FreeCore(); + } + + return; + } } + } - CsBindgen.NativeMethods.free_compile_result(_native); + private unsafe void FreeCore() + { + var native = _native; _native = default; + CsBindgen.NativeMethods.free_compile_result(native); if (_nativeByteCount > 0) { GC.RemoveMemoryPressure(_nativeByteCount); @@ -353,7 +416,7 @@ internal unsafe OutputStream(TypstDocument owner, byte* pointer, long length) /// public override unsafe int Read(Span buffer) { - ObjectDisposedException.ThrowIf(_owner.IsDisposed, _owner); + using var _ = _owner.EnterLease(); long position = Position; @@ -370,7 +433,6 @@ public override unsafe int Read(Span buffer) int count = (int)Math.Min((long)buffer.Length, available); new ReadOnlySpan(_pointer + position, count).CopyTo(buffer); Position = position + count; - GC.KeepAlive(_owner); return count; } @@ -384,10 +446,8 @@ public override int Read(byte[] buffer, int offset, int count) public override int ReadByte() { - ObjectDisposedException.ThrowIf(_owner.IsDisposed, _owner); - int value = base.ReadByte(); - GC.KeepAlive(_owner); - return value; + using var _ = _owner.EnterLease(); + return base.ReadByte(); } /// @@ -403,7 +463,7 @@ public override int ReadByte() public override unsafe void CopyTo(Stream destination, int bufferSize) { ValidateCopyToArguments(destination, bufferSize); - ObjectDisposedException.ThrowIf(_owner.IsDisposed, _owner); + using var _ = _owner.EnterLease(); long position = Position; long remaining = Length - position; @@ -416,8 +476,6 @@ public override unsafe void CopyTo(Stream destination, int bufferSize) remaining -= chunk; Position = position; } - - GC.KeepAlive(_owner); } ///