Issue Summary
Upgrading a project using Ghostscript.NET to .NET 6+ (including .NET 8/10) causes GhostscriptPipedOutput to throw a System.Runtime.InteropServices.SEHException originating from System.IO.Pipes.dll.
Root Cause
- Strict Native Handle Management in Modern .NET:
Starting with .NET 6+, System.IO.Pipes.dll manages unmanaged OS handles much more strictly. GhostscriptPipedOutput uses a legacy workaround (_pipe.ClientSafePipeHandle.SetHandleAsInvalid()), which causes invalid handle access and double-close conflicts when the Garbage Collector or Interop layer cleans up after Ghostscript closes the pipe on the unmanaged C side.
- Unsupported
Thread.Abort():
The Dispose() cleanup logic relies on _thread.Abort(), which is unsupported in modern .NET (.NET Core 1.0+ / .NET 5+) and causes a PlatformNotSupportedException or unexpected process state.
Fix / Resolution
Refactor GhostscriptPipedOutput to replace the legacy background Thread with an async/await Task-based reader (ReadAsync) and remove the manual SetHandleAsInvalid() hack in favor of safe handle disposal via DisposeLocalCopyOfClientHandle().
Example Code
I have changed the TargetFramework of Ghostscript.NET to netstandard2.1.
using System;
using System.Buffers;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
namespace Ghostscript.NET
{
/// <summary>
/// Thread-safe, high-performance Ghostscript piped output implementation for modern .NET (8/10+).
/// </summary>
public sealed class GhostscriptPipedOutput : IDisposable
{
private int _disposed; // 0 = false, 1 = true (for thread-safe Interlocked operations)
private readonly AnonymousPipeServerStream _pipe;
private readonly MemoryStream _data = new MemoryStream();
private readonly Task _readTask;
public GhostscriptPipedOutput()
{
_pipe = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable);
_readTask = Task.Run(ReadPipeAsync);
}
/// <summary>
/// Gets the client pipe handle string required by Ghostscript.
/// </summary>
public string ClientHandle => _pipe.GetClientHandleAsString();
private async Task ReadPipeAsync()
{
// Rent buffer from pool to minimize GC overhead
byte[] sharedBuffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
int readCount;
Memory<byte> memoryBuffer = sharedBuffer;
while ((readCount = await _pipe.ReadAsync(memoryBuffer, CancellationToken.None).ConfigureAwait(false)) > 0)
{
lock (_data)
{
// Utilize high-performance Span-based memory write
_data.Write(memoryBuffer[..readCount].Span);
}
}
}
catch (ObjectDisposedException)
{
// Expected when the pipe is disposed on the C# side
}
catch (IOException)
{
// Expected when Ghostscript closes its end of the pipe (EOF)
}
finally
{
ArrayPool<byte>.Shared.Return(sharedBuffer);
}
}
/// <summary>
/// Asynchronously retrieves the processed PDF bytes (Recommended).
/// </summary>
public async Task<byte[]> GetDataAsync()
{
CloseClientHandle();
await _readTask.ConfigureAwait(false);
lock (_data)
{
return _data.ToArray();
}
}
/// <summary>
/// Synchronous interface for legacy code integration.
/// </summary>
public byte[] Data
{
get
{
CloseClientHandle();
_readTask.GetAwaiter().GetResult();
lock (_data)
{
return _data.ToArray();
}
}
}
private void CloseClientHandle()
{
try
{
if (!_pipe.IsConnected)
{
_pipe.DisposeLocalCopyOfClientHandle();
}
}
catch
{
// Suppress disposal exceptions if handle was already released
}
}
/// <summary>
/// Releases all resources used by the GhostscriptPipedOutput instance.
/// </summary>
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) == 1)
return;
CloseClientHandle();
_pipe.Dispose();
try
{
// Wait for background read task to complete gracefully with a timeout
_readTask.Wait(TimeSpan.FromSeconds(2));
}
catch
{
// Suppress task timeout exceptions during disposal
}
_data.Dispose();
GC.SuppressFinalize(this);
}
}
}
Issue Summary
Upgrading a project using
Ghostscript.NETto .NET 6+ (including .NET 8/10) causesGhostscriptPipedOutputto throw aSystem.Runtime.InteropServices.SEHExceptionoriginating fromSystem.IO.Pipes.dll.Root Cause
Starting with .NET 6+,
System.IO.Pipes.dllmanages unmanaged OS handles much more strictly.GhostscriptPipedOutputuses a legacy workaround (_pipe.ClientSafePipeHandle.SetHandleAsInvalid()), which causes invalid handle access and double-close conflicts when the Garbage Collector or Interop layer cleans up after Ghostscript closes the pipe on the unmanaged C side.Thread.Abort():The
Dispose()cleanup logic relies on_thread.Abort(), which is unsupported in modern .NET (.NET Core 1.0+ / .NET 5+) and causes aPlatformNotSupportedExceptionor unexpected process state.Fix / Resolution
Refactor
GhostscriptPipedOutputto replace the legacy backgroundThreadwith anasync/awaitTask-based reader (ReadAsync) and remove the manualSetHandleAsInvalid()hack in favor of safe handle disposal viaDisposeLocalCopyOfClientHandle().Example Code
I have changed the
TargetFrameworkofGhostscript.NETtonetstandard2.1.