Files
regorus/bindings/csharp/Regorus/Utf8Marshaller.cs
Anand Krishnamoorthi 091bbb2e5c feat: Optimize C# binding interop (#488)
- Introduce Utf8Marshaller helpers and SafeHandle wrappers so the managed API centralizes UTF-8 conversions and lifetime management for native pointers.
- Update Engine, Compiler, CompiledPolicy, SchemaRegistry, and TargetRegistry to rely on the new marshaller/safe handles, tightening disposal and reducing transient allocations during interop calls.
- Add allocation guard coverage in Regorus.Tests and report bytes/op in the compiled policy benchmark to surface future regressions.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-10-27 12:35:40 -05:00

155 lines
4.7 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
#nullable enable
namespace Regorus.Internal
{
/// <summary>
/// Helpers for marshaling managed strings to null-terminated UTF-8 buffers.
/// Provides stack-based storage for short lived conversions and pooled backing
/// for longer lived pinned buffers.
/// </summary>
internal static class Utf8Marshaller
{
// Mirrors BCL patterns (e.g., System.Text.Json encoding helpers) by stackalloc'ing
// up to 512 bytes to cover common short strings while keeping the stack usage well
// below typical per-frame limits; larger payloads fall back to pooled buffers.
private const int StackAllocThreshold = 512;
/// <summary>
/// Represents a pooled and pinned UTF-8 buffer suitable for scenarios where
/// the pointer must remain stable beyond the immediate call site (for example,
/// when referenced by another buffer passed to native code).
/// </summary>
internal sealed class PinnedUtf8 : IDisposable
{
private GCHandle _handle;
private byte[]? _buffer;
private bool _disposed;
internal unsafe PinnedUtf8(string value)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
var byteCount = Encoding.UTF8.GetByteCount(value);
_buffer = ArrayPool<byte>.Shared.Rent(byteCount + 1);
try
{
var written = Encoding.UTF8.GetBytes(value, 0, value.Length, _buffer, 0);
_buffer[written] = 0;
_handle = GCHandle.Alloc(_buffer, GCHandleType.Pinned);
Pointer = (byte*)_handle.AddrOfPinnedObject();
Length = written + 1;
}
catch
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = null;
throw;
}
}
internal unsafe byte* Pointer { get; }
internal int Length { get; }
public void Dispose()
{
if (_disposed)
{
return;
}
if (_handle.IsAllocated)
{
_handle.Free();
}
if (_buffer != null)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = null;
}
_disposed = true;
}
}
internal unsafe delegate void Utf8PointerAction(byte* pointer);
internal static unsafe void WithUtf8(string value, Utf8PointerAction action)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
WithUtf8<object?>(value, ptr =>
{
action((byte*)ptr);
return null;
});
}
internal static T WithUtf8<T>(string value, Func<IntPtr, T> func)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
if (func is null)
{
throw new ArgumentNullException(nameof(func));
}
var byteCount = Encoding.UTF8.GetByteCount(value);
var required = byteCount + 1;
if (required <= StackAllocThreshold)
{
Span<byte> buffer = stackalloc byte[required];
return Invoke(value, func, buffer, byteCount);
}
var rented = ArrayPool<byte>.Shared.Rent(required);
try
{
Span<byte> buffer = rented;
return Invoke(value, func, buffer, byteCount);
}
finally
{
ArrayPool<byte>.Shared.Return(rented);
}
}
private static unsafe T Invoke<T>(string value, Func<IntPtr, T> func, Span<byte> buffer, int byteCount)
{
fixed (char* charPtr = value)
fixed (byte* bytePtr = buffer)
{
var written = Encoding.UTF8.GetBytes(charPtr, value.Length, bytePtr, byteCount);
bytePtr[written] = 0;
return func((IntPtr)bytePtr);
}
}
internal static PinnedUtf8 Pin(string value)
{
return new PinnedUtf8(value);
}
}
}