Question Details

No question body available.

Tags

c# .net-core pinvoke

Answers (2)

June 12, 2026 Score: 6 Rep: 531 Quality: Medium Completeness: 80%

You can pass a GCHandle to the unmanaged library, and retrieve it during your callbacks.

When creating the FTStream from a .NET stream, you wrap the stream:

    internal static unsafe FTStream ToFTStream(this Stream stream)
    {
        // FreeType requires ftStream to be in unmanaged memory
        FT_Stream ftStream = NativeMemory.AllocZeroed((nuint)sizeof(FTStream));
        GCHandle h = GCHandle.Alloc(stream);

ftStream->size = new CULong((nuint)stream.Length); ftStream->descriptor = (nint)h; ftStream->read = &
FTRead; ftStream->close = &FTClose;

return ftStream; }

You unwrap it in the callbacks used by FreeType:

    [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
    private unsafe static CULong FTRead(FT_Stream ftStream, CULong offset,
                                         byte buffer, CULong count)
    {
        GCHandle h = (GCHandle)(ftStream->descriptor);
        Stream stream = (Stream)(h.Target)!;

// rest of your code... }
June 12, 2026 Score: 1 Rep: 51 Quality: Low Completeness: 80%

Employ a GCHandle to connect the unmanaged FTStreamRec with the managed Stream, and then provide FreeType callbacks using static [UnmanageCallersOnly] methods.

The stream must be supported by FreeType:.

  1. Absolute offset for random access.

  2. A known total size.

  3. Reading into an unmanaged buffer

  4. a function that does not permit managed exceptions to be passed into native code.

using System.Runtime.CompilerServices; using System.Runtime.InteropServices;

internal sealed class StreamState { public required Stream Stream { get; init; } public bool LeaveOpen { get; init; } }

[StructLayout(LayoutKind.Explicit)] internal struct FTStreamDesc { [FieldOffset(0)] public nint pointer; [FieldOffset(0)] public CLong value; }

[StructLayout(LayoutKind.Sequential)] internal unsafe struct FTStream { public byte* base; public CULong size; public CULong pos;

public FTStreamDesc descriptor; public FTStreamDesc pathname;

public delegate unmanaged[Cdecl]< FT_Stream, CULong, byte, CULong, CULong> read;

public delegate unmanaged[Cdecl] close;

public nint memory; public byte cursor; public byte limit; }

The callbacks can recover the managed object from descriptor.pointer:

internal static unsafe class FreeTypeStreamCallbacks { [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] public static CULong Read( FTStream* ftStream, CULong offset, byte* buffer, CULong count) { try { var handle = GCHandle.FromIntPtr(ftStream->descriptor.pointer); var stream = ((StreamState)handle.Target!).Stream;

long requestedOffset = checked((long)offset.Value);

if (stream.Position != requestedOffset) stream.Seek(requestedOffset, SeekOrigin.Begin);

// A zero count means "perform a seek". if (count.Value == 0) return new CULong(0);

int requested = checked((int)count.Value); int read = stream.Read(new Span(buffer, requested));

ftStream->pos = new CULong((ulong)stream.Position); return new CULong((ulong)read); } catch { // For count > 0, zero indicates no bytes were read. // For count == 0, non-zero indicates seek failure. return count.Value == 0 ? new CULong(1) : new CULong(0); } }

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] public static void Close(FTStream ftStream) { nint pointer = ftStream->descriptor.pointer;

if (pointer == 0) return;

ftStream->descriptor.pointer = 0;

var handle = GCHandle.FromIntPtr(pointer);

try { var state = (StreamState)handle.Target!;

if (!state.LeaveOpen) state.Stream.Dispose(); } catch { // Exceptions must not cross the unmanaged boundary. } finally { handle.Free(); } } }

Create the native stream record and call FT_Open_Face:

internal static unsafe nint OpenFace(nint library, Stream stream) { if (!stream.CanSeek) throw new ArgumentException("FreeType requires a seekable stream.");

var state = new StreamState { Stream = stream, LeaveOpen = false };

var handle = GCHandle.Alloc(state);

FT_Stream ftStream = (FTStream*)NativeMemory.AllocZeroed((nuint)sizeof(FTStream));

ftStream->size = new CULong(checked((ulong)stream.Length)); ftStream->descriptor.pointer = GCHandle.ToIntPtr(handle); ftStream->read = &FreeTypeStreamCallbacks.Read; ftStream->close = &FreeTypeStreamCallbacks.Close;

FTOpenArgs args = default; args.flags = 0x2; // FTOPENSTREAM args.stream = ftStream;

nint face; int error = FTOpenFace(library, &args, new CLong(0), &face);

if (error != 0) { // FreeType invokes close on FTOpenFace failure. NativeMemory.Free(ftStream); throw new InvalidOperationException($"FTOpenFace failed: {error}"); }

// Keep ftStream allocated until after FTDoneFace(face). return face; }

The returned face must be wrapped together with ftStream. During disposal:

FTDoneFace(face); // Invokes the close callback. NativeMemory.Free(ftStream);