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>
This commit is contained in:
Anand Krishnamoorthi
2025-10-27 12:35:40 -05:00
committed by GitHub
parent 1e4ff952e6
commit 091bbb2e5c
9 changed files with 771 additions and 216 deletions

View File

@@ -129,12 +129,12 @@ namespace Benchmarks
Console.WriteLine($"Warming up with {threads} threads for {warmupSeconds} seconds..."); Console.WriteLine($"Warming up with {threads} threads for {warmupSeconds} seconds...");
// Warmup phase // Warmup phase
var (_, _, _) = RunBenchmarkPhase(threads, warmupSeconds, policiesWithInputs, compiledPolicies, useSharedPolicies, isWarmup: true); var (_, _, _, _) = RunBenchmarkPhase(threads, warmupSeconds, policiesWithInputs, compiledPolicies, useSharedPolicies, isWarmup: true);
Console.WriteLine($"Running benchmark with {threads} threads for {durationSeconds} seconds..."); Console.WriteLine($"Running benchmark with {threads} threads for {durationSeconds} seconds...");
// Actual benchmark phase // Actual benchmark phase
var (totalEvaluations, evaluationTime, policyCounters) = RunBenchmarkPhase(threads, durationSeconds, policiesWithInputs, compiledPolicies, useSharedPolicies, isWarmup: false); var (totalEvaluations, evaluationTime, policyCounters, allocatedBytes) = RunBenchmarkPhase(threads, durationSeconds, policiesWithInputs, compiledPolicies, useSharedPolicies, isWarmup: false);
// Calculate throughput based on pure evaluation time (consistent with Rust benchmark) // Calculate throughput based on pure evaluation time (consistent with Rust benchmark)
var evalsPerSecond = totalEvaluations / evaluationTime.TotalSeconds; var evalsPerSecond = totalEvaluations / evaluationTime.TotalSeconds;
@@ -144,6 +144,12 @@ namespace Benchmarks
Console.WriteLine($" time: [{evaluationTime.TotalMilliseconds:F2} ms]"); Console.WriteLine($" time: [{evaluationTime.TotalMilliseconds:F2} ms]");
Console.WriteLine($" thrpt: [{kelemsPerSecond:F2} Kelem/s]"); Console.WriteLine($" thrpt: [{kelemsPerSecond:F2} Kelem/s]");
if (totalEvaluations > 0)
{
var bytesPerEval = allocatedBytes / (double)totalEvaluations;
Console.WriteLine($" alloc: [{bytesPerEval:F2} B/op] (total {allocatedBytes} B)");
}
// Clean up compiled policies if we created them // Clean up compiled policies if we created them
if (compiledPolicies != null) if (compiledPolicies != null)
{ {
@@ -166,7 +172,7 @@ namespace Benchmarks
} }
} }
private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary<string, int> policyCounters) RunBenchmarkPhase( private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary<string, int> policyCounters, long allocatedBytes) RunBenchmarkPhase(
int threads, int threads,
int durationSeconds, int durationSeconds,
List<(string Policy, string[] Inputs)> policiesWithInputs, List<(string Policy, string[] Inputs)> policiesWithInputs,
@@ -180,6 +186,7 @@ namespace Benchmarks
var evaluationTimes = new Dictionary<int, TimeSpan>(); var evaluationTimes = new Dictionary<int, TimeSpan>();
var lockObject = new object(); var lockObject = new object();
var stopExecution = false; var stopExecution = false;
long allocatedBytes = 0;
// Initialize counters // Initialize counters
foreach (var policyName in PolicyNames) foreach (var policyName in PolicyNames)
@@ -194,6 +201,12 @@ namespace Benchmarks
int tid = threadId; int tid = threadId;
tasks[threadId] = Task.Run(() => tasks[threadId] = Task.Run(() =>
{ {
long allocationStart = 0;
if (!isWarmup)
{
allocationStart = GC.GetAllocatedBytesForCurrentThread();
}
barrier.SignalAndWait(); barrier.SignalAndWait();
int evaluationCount = 0; int evaluationCount = 0;
@@ -256,6 +269,9 @@ namespace Benchmarks
evaluationTimes[tid] = TimeSpan.Zero; evaluationTimes[tid] = TimeSpan.Zero;
evaluationTimes[tid] = localEvaluationTime; evaluationTimes[tid] = localEvaluationTime;
} }
var allocationEnd = GC.GetAllocatedBytesForCurrentThread();
System.Threading.Interlocked.Add(ref allocatedBytes, allocationEnd - allocationStart);
} }
}); });
} }
@@ -272,7 +288,7 @@ namespace Benchmarks
// Use pure evaluation time (consistent with Rust benchmark) // Use pure evaluation time (consistent with Rust benchmark)
var evaluationTime = totalEvaluationTime == TimeSpan.Zero ? stopwatch.Elapsed : totalEvaluationTime; var evaluationTime = totalEvaluationTime == TimeSpan.Zero ? stopwatch.Elapsed : totalEvaluationTime;
return (totalEvaluations, evaluationTime, policyCounters); return (totalEvaluations, evaluationTime, policyCounters, allocatedBytes);
} }
} }
} }

View File

@@ -1,10 +1,12 @@
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License. // Licensed under the MIT License.
namespace Regorus.Tests; using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Regorus;
namespace Regorus.Tests;
[TestClass] [TestClass]
public class RegorusTests public class RegorusTests
@@ -212,4 +214,43 @@ public class RegorusTests
Assert.AreEqual("a", parameters![0]["parameters"][0]["name"].ToString()); Assert.AreEqual("a", parameters![0]["parameters"][0]["name"].ToString());
Assert.AreEqual("b", parameters![0]["modifiers"][0]["name"].ToString()); Assert.AreEqual("b", parameters![0]["modifiers"][0]["name"].ToString());
} }
[TestMethod]
public void SetInputJson_has_negligible_allocations_after_warmup()
{
using var engine = new Engine();
const string payload = "{}";
// Warm up the engine and JIT to ensure subsequent measurements are representative.
for (int i = 0; i < 16; i++)
{
engine.SetInputJson(payload);
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
const int iterations = 256;
var before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < iterations; i++)
{
engine.SetInputJson(payload);
}
var after = GC.GetAllocatedBytesForCurrentThread();
var allocated = Math.Max(0, after - before);
var bytesPerOp = allocated / (double)iterations;
// Runtime bookkeeping (delegate caches, GC write barriers) differs across platforms, so
// we measure bytes per call rather than absolute totals and allow a small budget.
// CI will flag regressions where marshalling starts allocating per invocation.
// Allow a small budget for delegates and runtime bookkeeping while still flagging regressions.
Assert.IsTrue(
bytesPerOp <= 512,
$"Expected ≤512 B/op after warmup, but observed {bytesPerOp:F2} B/op (total {allocated} bytes)."
);
}
} }

View File

@@ -4,6 +4,7 @@
using System; using System;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading;
#nullable enable #nullable enable
namespace Regorus namespace Regorus
@@ -23,13 +24,14 @@ namespace Regorus
/// </summary> /// </summary>
public unsafe sealed class CompiledPolicy : IDisposable public unsafe sealed class CompiledPolicy : IDisposable
{ {
private Internal.RegorusCompiledPolicy* _policy; private RegorusCompiledPolicyHandle? _handle;
private int _isDisposed; private readonly ManualResetEventSlim _idleEvent = new(initialState: true);
private int _activeEvaluations; private int _isDisposed;
private int _activeEvaluations;
internal CompiledPolicy(Internal.RegorusCompiledPolicy* policy) internal CompiledPolicy(RegorusCompiledPolicyHandle handle)
{ {
_policy = policy; _handle = handle ?? throw new ArgumentNullException(nameof(handle));
} }
/// <summary> /// <summary>
@@ -44,21 +46,34 @@ namespace Regorus
public string? EvalWithInput(string inputJson) public string? EvalWithInput(string inputJson)
{ {
// Increment active evaluations count // Increment active evaluations count
System.Threading.Interlocked.Increment(ref _activeEvaluations); var active = System.Threading.Interlocked.Increment(ref _activeEvaluations);
if (active == 1)
{
_idleEvent.Reset();
}
try try
{ {
ThrowIfDisposed(); ThrowIfDisposed();
var inputBytes = Encoding.UTF8.GetBytes(inputJson + char.MinValue); return Internal.Utf8Marshaller.WithUtf8(inputJson, inputPtr =>
fixed (byte* inputPtr = inputBytes)
{ {
return CheckAndDropResult(Internal.API.regorus_compiled_policy_eval_with_input(_policy, inputPtr)); return UseHandle(policyPtr =>
} {
unsafe
{
return CheckAndDropResult(Internal.API.regorus_compiled_policy_eval_with_input((Internal.RegorusCompiledPolicy*)policyPtr, (byte*)inputPtr));
}
});
});
} }
finally finally
{ {
// Decrement active evaluations count // Decrement active evaluations count
System.Threading.Interlocked.Decrement(ref _activeEvaluations); var remaining = System.Threading.Interlocked.Decrement(ref _activeEvaluations);
if (remaining == 0)
{
_idleEvent.Set();
}
} }
} }
@@ -72,7 +87,13 @@ namespace Regorus
public PolicyInfo GetPolicyInfo() public PolicyInfo GetPolicyInfo()
{ {
ThrowIfDisposed(); ThrowIfDisposed();
var jsonResult = CheckAndDropResult(Internal.API.regorus_compiled_policy_get_policy_info(_policy)); var jsonResult = UseHandle(policyPtr =>
{
unsafe
{
return CheckAndDropResult(Internal.API.regorus_compiled_policy_get_policy_info((Internal.RegorusCompiledPolicy*)policyPtr));
}
});
if (string.IsNullOrEmpty(jsonResult)) if (string.IsNullOrEmpty(jsonResult))
{ {
@@ -105,25 +126,22 @@ namespace Regorus
{ {
if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{ {
if (_policy != null) var handle = _handle;
if (handle != null)
{ {
// Wait for all active evaluations to complete _idleEvent.Wait();
while (System.Threading.Volatile.Read(ref _activeEvaluations) > 0)
{
System.Threading.Thread.Yield();
}
Internal.API.regorus_compiled_policy_drop(_policy); handle.Dispose();
_policy = null; _handle = null;
} }
_idleEvent.Dispose();
} }
} }
~CompiledPolicy() => Dispose(disposing: false);
private void ThrowIfDisposed() private void ThrowIfDisposed()
{ {
if (_isDisposed != 0) if (_isDisposed != 0 || _handle is null || _handle.IsClosed)
throw new ObjectDisposedException(nameof(CompiledPolicy)); throw new ObjectDisposedException(nameof(CompiledPolicy));
} }
@@ -164,5 +182,39 @@ namespace Regorus
Internal.API.regorus_result_drop(result); Internal.API.regorus_result_drop(result);
} }
} }
private RegorusCompiledPolicyHandle GetHandleForUse()
{
var handle = _handle;
if (handle is null || handle.IsClosed || handle.IsInvalid)
{
throw new ObjectDisposedException(nameof(CompiledPolicy));
}
return handle;
}
private T UseHandle<T>(Func<IntPtr, T> func)
{
var handle = GetHandleForUse();
bool addedRef = false;
try
{
handle.DangerousAddRef(ref addedRef);
var pointer = handle.DangerousGetHandle();
if (pointer == IntPtr.Zero)
{
throw new ObjectDisposedException(nameof(CompiledPolicy));
}
return func(pointer);
}
finally
{
if (addedRef)
{
handle.DangerousRelease();
}
}
}
} }
} }

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Regorus.Internal;
#nullable enable #nullable enable
namespace Regorus namespace Regorus
@@ -54,49 +55,48 @@ namespace Regorus
/// <exception cref="Exception">Thrown when compilation fails</exception> /// <exception cref="Exception">Thrown when compilation fails</exception>
public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IEnumerable<PolicyModule> modules, string entryPointRule) public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IEnumerable<PolicyModule> modules, string entryPointRule)
{ {
var dataBytes = Encoding.UTF8.GetBytes(dataJson + char.MinValue);
var entryPointBytes = Encoding.UTF8.GetBytes(entryPointRule + char.MinValue);
var modulesArray = modules.ToArray(); var modulesArray = modules.ToArray();
// Convert C# modules to native structs
var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length]; var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length];
var pinnedHandles = new List<GCHandle>(); var pinnedStrings = new List<Utf8Marshaller.PinnedUtf8>(modulesArray.Length * 2);
try try
{ {
for (int i = 0; i < modulesArray.Length; i++) for (int i = 0; i < modulesArray.Length; i++)
{ {
var idBytes = Encoding.UTF8.GetBytes(modulesArray[i].Id + char.MinValue); var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id);
var contentBytes = Encoding.UTF8.GetBytes(modulesArray[i].Content + char.MinValue); var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content);
pinnedStrings.Add(idPinned);
var idHandle = GCHandle.Alloc(idBytes, GCHandleType.Pinned); pinnedStrings.Add(contentPinned);
var contentHandle = GCHandle.Alloc(contentBytes, GCHandleType.Pinned);
pinnedHandles.Add(idHandle);
pinnedHandles.Add(contentHandle);
nativeModules[i] = new Internal.RegorusPolicyModule nativeModules[i] = new Internal.RegorusPolicyModule
{ {
id = (byte*)idHandle.AddrOfPinnedObject(), id = idPinned.Pointer,
content = (byte*)contentHandle.AddrOfPinnedObject() content = contentPinned.Pointer
}; };
} }
fixed (byte* dataPtr = dataBytes) return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
fixed (byte* entryPointPtr = entryPointBytes) Utf8Marshaller.WithUtf8(entryPointRule, entryPointPtr =>
fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules) {
{ unsafe
var result = Internal.API.regorus_compile_policy_with_entrypoint( {
dataPtr, modulesPtr, (UIntPtr)modulesArray.Length, entryPointPtr); fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules)
{
var result = Internal.API.regorus_compile_policy_with_entrypoint(
(byte*)dataPtr, modulesPtr, (UIntPtr)modulesArray.Length, (byte*)entryPointPtr);
var policy = GetCompiledPolicyResult(result); var policy = GetCompiledPolicyResult(result);
return policy; return policy;
} }
}
}));
} }
finally finally
{ {
foreach (var handle in pinnedHandles) foreach (var pinned in pinnedStrings)
{ {
handle.Free(); pinned.Dispose();
} }
} }
} }
@@ -112,47 +112,47 @@ namespace Regorus
/// <exception cref="Exception">Thrown when compilation fails</exception> /// <exception cref="Exception">Thrown when compilation fails</exception>
public static CompiledPolicy CompilePolicyForTarget(string dataJson, IEnumerable<PolicyModule> modules) public static CompiledPolicy CompilePolicyForTarget(string dataJson, IEnumerable<PolicyModule> modules)
{ {
var dataBytes = Encoding.UTF8.GetBytes(dataJson + char.MinValue);
var modulesArray = modules.ToArray(); var modulesArray = modules.ToArray();
// Convert C# modules to native structs
var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length]; var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length];
var pinnedHandles = new List<GCHandle>(); var pinnedStrings = new List<Utf8Marshaller.PinnedUtf8>(modulesArray.Length * 2);
try try
{ {
for (int i = 0; i < modulesArray.Length; i++) for (int i = 0; i < modulesArray.Length; i++)
{ {
var idBytes = Encoding.UTF8.GetBytes(modulesArray[i].Id + char.MinValue); var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id);
var contentBytes = Encoding.UTF8.GetBytes(modulesArray[i].Content + char.MinValue); var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content);
pinnedStrings.Add(idPinned);
var idHandle = GCHandle.Alloc(idBytes, GCHandleType.Pinned); pinnedStrings.Add(contentPinned);
var contentHandle = GCHandle.Alloc(contentBytes, GCHandleType.Pinned);
pinnedHandles.Add(idHandle);
pinnedHandles.Add(contentHandle);
nativeModules[i] = new Internal.RegorusPolicyModule nativeModules[i] = new Internal.RegorusPolicyModule
{ {
id = (byte*)idHandle.AddrOfPinnedObject(), id = idPinned.Pointer,
content = (byte*)contentHandle.AddrOfPinnedObject() content = contentPinned.Pointer
}; };
} }
fixed (byte* dataPtr = dataBytes) return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules)
{ {
var result = Internal.API.regorus_compile_policy_for_target( unsafe
dataPtr, modulesPtr, (UIntPtr)modulesArray.Length); {
fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules)
{
var result = Internal.API.regorus_compile_policy_for_target(
(byte*)dataPtr, modulesPtr, (UIntPtr)modulesArray.Length);
var policy = GetCompiledPolicyResult(result); var policy = GetCompiledPolicyResult(result);
return policy; return policy;
} }
}
});
} }
finally finally
{ {
foreach (var handle in pinnedHandles) foreach (var pinned in pinnedStrings)
{ {
handle.Free(); pinned.Dispose();
} }
} }
} }
@@ -185,7 +185,8 @@ namespace Regorus
throw new Exception("Expected compiled policy pointer but got different data type"); throw new Exception("Expected compiled policy pointer but got different data type");
} }
return new CompiledPolicy((Internal.RegorusCompiledPolicy*)result.pointer_value); var handle = RegorusCompiledPolicyHandle.FromPointer((IntPtr)result.pointer_value);
return new CompiledPolicy(handle);
} }
finally finally
{ {

View File

@@ -4,6 +4,7 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Regorus.Internal;
#nullable enable #nullable enable
@@ -15,17 +16,14 @@ namespace Regorus
/// Cloning is cheap and involves only incrementing reference counts for shared immutable objects like parsed policies, /// Cloning is cheap and involves only incrementing reference counts for shared immutable objects like parsed policies,
/// data etc. Mutable state is deep copied as needed. /// data etc. Mutable state is deep copied as needed.
/// </summary> /// </summary>
public unsafe sealed class Engine : System.IDisposable public unsafe sealed class Engine : IDisposable
{ {
private Regorus.Internal.RegorusEngine* E; private RegorusEngineHandle? _handle;
// Detect redundant Dispose() calls in a thread-safe manner. private int _isDisposed;
// _isDisposed == 0 means Dispose(bool) has not been called yet.
// _isDisposed == 1 means Dispose(bool) has been already called.
private int isDisposed;
public Engine() public Engine()
{ {
E = Regorus.Internal.API.regorus_engine_new(); _handle = RegorusEngineHandle.Create();
} }
public void Dispose() public void Dispose()
@@ -49,182 +47,314 @@ namespace Regorus
// other objects. Only unmanaged resources can be disposed. // other objects. Only unmanaged resources can be disposed.
void Dispose(bool disposing) void Dispose(bool disposing)
{ {
// In case _isDisposed is 0, atomically set it to 1. if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
// Enter the branch only if the original value is 0.
if (System.Threading.Interlocked.CompareExchange(ref isDisposed, 1, 0) == 0)
{ {
// If disposing equals true, dispose all managed _handle?.Dispose();
// and unmanaged resources. _handle = null;
if (disposing)
{
// No managed resource to dispose.
}
// Call the appropriate methods to clean up
// unmanaged resources here.
// If disposing is false,
// only the following code is executed.
if (E != null)
{
Regorus.Internal.API.regorus_engine_drop(E);
E = null;
}
} }
} }
// Use C# finalizer syntax for finalization code. private Engine(RegorusEngineHandle handle)
// This finalizer will run only if the Dispose method
// does not get called.
~Engine() => Dispose(disposing: false);
// Helper for implementing Clone
private Engine(Internal.RegorusEngine* engine)
{ {
this.E = engine; _handle = handle ?? throw new ArgumentNullException(nameof(handle));
} }
public Engine Clone() => new(Internal.API.regorus_engine_clone(E)); public Engine Clone()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
var clonePtr = Regorus.Internal.API.regorus_engine_clone((Regorus.Internal.RegorusEngine*)enginePtr);
if (clonePtr is null)
{
throw new InvalidOperationException("Failed to clone Regorus engine.");
}
var handle = RegorusEngineHandle.FromPointer((IntPtr)clonePtr);
return new Engine(handle);
}
});
}
public void SetStrictBuiltinErrors(bool strict) public void SetStrictBuiltinErrors(bool strict)
{ {
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_strict_builtin_errors(E, strict)); ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_strict_builtin_errors((Regorus.Internal.RegorusEngine*)enginePtr, strict));
}
});
} }
byte[] NullTerminatedUTF8Bytes(string s)
{
return Encoding.UTF8.GetBytes(s + char.MinValue);
}
public string? AddPolicy(string path, string rego) public string? AddPolicy(string path, string rego)
{ {
var pathBytes = NullTerminatedUTF8Bytes(path); ThrowIfDisposed();
var regoBytes = NullTerminatedUTF8Bytes(rego); return Utf8Marshaller.WithUtf8(path, pathPtr =>
Utf8Marshaller.WithUtf8(rego, regoPtr =>
fixed (byte* pathPtr = pathBytes)
{
fixed (byte* regoPtr = regoBytes)
{ {
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy(E, pathPtr, regoPtr)); unsafe
} {
} return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr, (byte*)regoPtr));
}
});
}
}));
} }
public void SetRegoV0(bool enable) public void SetRegoV0(bool enable)
{ {
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_rego_v0(E, enable)); ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_rego_v0((Regorus.Internal.RegorusEngine*)enginePtr, enable));
}
});
} }
public string? AddPolicyFromFile(string path) public string? AddPolicyFromFile(string path)
{ {
var pathBytes = NullTerminatedUTF8Bytes(path); ThrowIfDisposed();
fixed (byte* pathPtr = pathBytes) return Utf8Marshaller.WithUtf8(path, pathPtr =>
{ {
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy_from_file(E, pathPtr)); unsafe
} {
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy_from_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
}
});
}
});
} }
public void AddDataJson(string data) public void AddDataJson(string data)
{ {
var dataBytes = NullTerminatedUTF8Bytes(data); ThrowIfDisposed();
fixed (byte* dataPtr = dataBytes) Utf8Marshaller.WithUtf8(data, dataPtr =>
{ {
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_json(E, dataPtr)); unsafe
} {
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)dataPtr));
}
});
}
});
} }
public void AddDataFromJsonFile(string path) public void AddDataFromJsonFile(string path)
{ {
var pathBytes = NullTerminatedUTF8Bytes(path); ThrowIfDisposed();
fixed (byte* pathPtr = pathBytes) Utf8Marshaller.WithUtf8(path, pathPtr =>
{ {
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_from_json_file(E, pathPtr)); unsafe
} {
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
}
});
}
});
} }
public void SetInputJson(string input) public void SetInputJson(string input)
{ {
var inputBytes = NullTerminatedUTF8Bytes(input); ThrowIfDisposed();
fixed (byte* inputPtr = inputBytes) Utf8Marshaller.WithUtf8(input, inputPtr =>
{ {
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_json(E, inputPtr)); unsafe
} {
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)inputPtr));
}
});
}
});
} }
public void SetInputFromJsonFile(string path) public void SetInputFromJsonFile(string path)
{ {
var pathBytes = NullTerminatedUTF8Bytes(path); ThrowIfDisposed();
fixed (byte* pathPtr = pathBytes) Utf8Marshaller.WithUtf8(path, pathPtr =>
{ {
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_from_json_file(E, pathPtr)); unsafe
} {
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
}
});
}
});
} }
public string? EvalQuery(string query) public string? EvalQuery(string query)
{ {
var queryBytes = NullTerminatedUTF8Bytes(query); ThrowIfDisposed();
fixed (byte* queryPtr = queryBytes) return Utf8Marshaller.WithUtf8(query, queryPtr =>
{ {
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_query(E, queryPtr)); unsafe
} {
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_query((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)queryPtr));
}
});
}
});
} }
public string? EvalRule(string rule) public string? EvalRule(string rule)
{ {
var ruleBytes = NullTerminatedUTF8Bytes(rule); ThrowIfDisposed();
fixed (byte* rulePtr = ruleBytes) return Utf8Marshaller.WithUtf8(rule, rulePtr =>
{ {
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_rule(E, rulePtr)); unsafe
} {
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_rule((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)rulePtr));
}
});
}
});
} }
public void SetEnableCoverage(bool enable) public void SetEnableCoverage(bool enable)
{ {
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_enable_coverage(E, enable)); ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_enable_coverage((Regorus.Internal.RegorusEngine*)enginePtr, enable));
}
});
} }
public void ClearCoverageData() public void ClearCoverageData()
{ {
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_coverage_data(E)); ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_coverage_data((Regorus.Internal.RegorusEngine*)enginePtr));
}
});
} }
public string? GetCoverageReport() public string? GetCoverageReport()
{ {
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report(E)); ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report((Regorus.Internal.RegorusEngine*)enginePtr));
}
});
} }
public string? GetCoverageReportPretty() public string? GetCoverageReportPretty()
{ {
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report_pretty(E)); ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report_pretty((Regorus.Internal.RegorusEngine*)enginePtr));
}
});
} }
public void SetGatherPrints(bool enable) public void SetGatherPrints(bool enable)
{ {
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_gather_prints(E, enable)); ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_gather_prints((Regorus.Internal.RegorusEngine*)enginePtr, enable));
}
});
} }
public string? TakePrints() public string? TakePrints()
{ {
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_take_prints(E)); ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_take_prints((Regorus.Internal.RegorusEngine*)enginePtr));
}
});
} }
public string? GetAstAsJson() public string? GetAstAsJson()
{ {
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_ast_as_json(E)); ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_ast_as_json((Regorus.Internal.RegorusEngine*)enginePtr));
}
});
} }
public string? GetPolicyPackageNames() public string? GetPolicyPackageNames()
{ {
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_package_names(E)); ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_package_names((Regorus.Internal.RegorusEngine*)enginePtr));
}
});
} }
public string? GetPolicyParameters() public string? GetPolicyParameters()
{ {
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_parameters(E)); ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_parameters((Regorus.Internal.RegorusEngine*)enginePtr));
}
});
} }
string? StringFromUTF8(IntPtr ptr) string? StringFromUTF8(IntPtr ptr)
@@ -260,5 +390,56 @@ namespace Regorus
return resultString; return resultString;
} }
private void ThrowIfDisposed()
{
if (_isDisposed != 0 || _handle is null || _handle.IsClosed)
{
throw new ObjectDisposedException(nameof(Engine));
}
}
private RegorusEngineHandle GetHandleForUse()
{
var handle = _handle;
if (handle is null || handle.IsClosed || handle.IsInvalid)
{
throw new ObjectDisposedException(nameof(Engine));
}
return handle;
}
private void UseHandle(Action<IntPtr> action)
{
UseHandle<object?>(handlePtr =>
{
action(handlePtr);
return null;
});
}
private T UseHandle<T>(Func<IntPtr, T> func)
{
var handle = GetHandleForUse();
bool addedRef = false;
try
{
handle.DangerousAddRef(ref addedRef);
var pointer = handle.DangerousGetHandle();
if (pointer == IntPtr.Zero)
{
throw new ObjectDisposedException(nameof(Engine));
}
return func(pointer);
}
finally
{
if (addedRef)
{
handle.DangerousRelease();
}
}
}
} }
} }

View File

@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
#nullable enable
namespace Regorus
{
internal sealed class RegorusEngineHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private RegorusEngineHandle() : base(ownsHandle: true)
{
}
internal static RegorusEngineHandle Create()
{
unsafe
{
var raw = Internal.API.regorus_engine_new();
if (raw is null)
{
throw new InvalidOperationException("Failed to create Regorus engine.");
}
var handle = new RegorusEngineHandle();
handle.SetHandle((IntPtr)raw);
return handle;
}
}
internal static RegorusEngineHandle FromPointer(IntPtr pointer)
{
if (pointer == IntPtr.Zero)
{
throw new ArgumentException("Pointer cannot be zero.", nameof(pointer));
}
var handle = new RegorusEngineHandle();
handle.SetHandle(pointer);
return handle;
}
protected override bool ReleaseHandle()
{
if (!IsInvalid && !IsClosed)
{
unsafe
{
Internal.API.regorus_engine_drop((Internal.RegorusEngine*)handle);
}
SetHandle(IntPtr.Zero);
}
return true;
}
}
internal sealed class RegorusCompiledPolicyHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private RegorusCompiledPolicyHandle() : base(ownsHandle: true)
{
}
internal static RegorusCompiledPolicyHandle FromPointer(IntPtr pointer)
{
if (pointer == IntPtr.Zero)
{
throw new ArgumentException("Pointer cannot be zero.", nameof(pointer));
}
var handle = new RegorusCompiledPolicyHandle();
handle.SetHandle(pointer);
return handle;
}
protected override bool ReleaseHandle()
{
if (!IsInvalid && !IsClosed)
{
unsafe
{
Internal.API.regorus_compiled_policy_drop((Internal.RegorusCompiledPolicy*)handle);
}
SetHandle(IntPtr.Zero);
}
return true;
}
}
}

View File

@@ -3,6 +3,7 @@
using System; using System;
using System.Text; using System.Text;
using Regorus.Internal;
#nullable enable #nullable enable
namespace Regorus namespace Regorus
@@ -21,14 +22,16 @@ namespace Regorus
/// <exception cref="Exception">Thrown when schema registration fails</exception> /// <exception cref="Exception">Thrown when schema registration fails</exception>
public static void RegisterResource(string name, string schemaJson) public static void RegisterResource(string name, string schemaJson)
{ {
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); Utf8Marshaller.WithUtf8(name, namePtr =>
var schemaBytes = Encoding.UTF8.GetBytes(schemaJson + char.MinValue);
fixed (byte* namePtr = nameBytes)
fixed (byte* schemaPtr = schemaBytes)
{ {
CheckAndDropResult(Internal.API.regorus_resource_schema_register(namePtr, schemaPtr)); Utf8Marshaller.WithUtf8(schemaJson, schemaPtr =>
} {
unsafe
{
CheckAndDropResult(Internal.API.regorus_resource_schema_register((byte*)namePtr, (byte*)schemaPtr));
}
});
});
} }
/// <summary> /// <summary>
@@ -39,12 +42,14 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception> /// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool ContainsResource(string name) public static bool ContainsResource(string name)
{ {
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); return Utf8Marshaller.WithUtf8(name, namePtr =>
fixed (byte* namePtr = nameBytes)
{ {
var result = Internal.API.regorus_resource_schema_contains(namePtr); unsafe
return GetBoolResult(result); {
} var result = Internal.API.regorus_resource_schema_contains((byte*)namePtr);
return GetBoolResult(result);
}
});
} }
/// <summary> /// <summary>
@@ -93,12 +98,14 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception> /// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool RemoveResource(string name) public static bool RemoveResource(string name)
{ {
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); return Utf8Marshaller.WithUtf8(name, namePtr =>
fixed (byte* namePtr = nameBytes)
{ {
var result = Internal.API.regorus_resource_schema_remove(namePtr); unsafe
return GetBoolResult(result); {
} var result = Internal.API.regorus_resource_schema_remove((byte*)namePtr);
return GetBoolResult(result);
}
});
} }
/// <summary> /// <summary>
@@ -118,14 +125,16 @@ namespace Regorus
/// <exception cref="Exception">Thrown when schema registration fails</exception> /// <exception cref="Exception">Thrown when schema registration fails</exception>
public static void RegisterEffect(string name, string schemaJson) public static void RegisterEffect(string name, string schemaJson)
{ {
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); Utf8Marshaller.WithUtf8(name, namePtr =>
var schemaBytes = Encoding.UTF8.GetBytes(schemaJson + char.MinValue);
fixed (byte* namePtr = nameBytes)
fixed (byte* schemaPtr = schemaBytes)
{ {
CheckAndDropResult(Internal.API.regorus_effect_schema_register(namePtr, schemaPtr)); Utf8Marshaller.WithUtf8(schemaJson, schemaPtr =>
} {
unsafe
{
CheckAndDropResult(Internal.API.regorus_effect_schema_register((byte*)namePtr, (byte*)schemaPtr));
}
});
});
} }
/// <summary> /// <summary>
@@ -136,12 +145,14 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception> /// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool ContainsEffect(string name) public static bool ContainsEffect(string name)
{ {
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); return Utf8Marshaller.WithUtf8(name, namePtr =>
fixed (byte* namePtr = nameBytes)
{ {
var result = Internal.API.regorus_effect_schema_contains(namePtr); unsafe
return GetBoolResult(result); {
} var result = Internal.API.regorus_effect_schema_contains((byte*)namePtr);
return GetBoolResult(result);
}
});
} }
/// <summary> /// <summary>
@@ -190,12 +201,14 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception> /// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool RemoveEffect(string name) public static bool RemoveEffect(string name)
{ {
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); return Utf8Marshaller.WithUtf8(name, namePtr =>
fixed (byte* namePtr = nameBytes)
{ {
var result = Internal.API.regorus_effect_schema_remove(namePtr); unsafe
return GetBoolResult(result); {
} var result = Internal.API.regorus_effect_schema_remove((byte*)namePtr);
return GetBoolResult(result);
}
});
} }
/// <summary> /// <summary>

View File

@@ -3,6 +3,7 @@
using System; using System;
using System.Text; using System.Text;
using Regorus.Internal;
#nullable enable #nullable enable
namespace Regorus namespace Regorus
@@ -22,11 +23,13 @@ namespace Regorus
/// <exception cref="Exception">Thrown when target registration fails</exception> /// <exception cref="Exception">Thrown when target registration fails</exception>
public static void RegisterFromJson(string targetJson) public static void RegisterFromJson(string targetJson)
{ {
var targetBytes = Encoding.UTF8.GetBytes(targetJson + char.MinValue); Utf8Marshaller.WithUtf8(targetJson, targetPtr =>
fixed (byte* targetPtr = targetBytes)
{ {
CheckAndDropResult(Internal.API.regorus_register_target_from_json(targetPtr)); unsafe
} {
CheckAndDropResult(Internal.API.regorus_register_target_from_json((byte*)targetPtr));
}
});
} }
/// <summary> /// <summary>
@@ -37,12 +40,14 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception> /// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool Contains(string name) public static bool Contains(string name)
{ {
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); return Utf8Marshaller.WithUtf8(name, namePtr =>
fixed (byte* namePtr = nameBytes)
{ {
var result = Internal.API.regorus_target_registry_contains(namePtr); unsafe
return GetBoolResult(result); {
} var result = Internal.API.regorus_target_registry_contains((byte*)namePtr);
return GetBoolResult(result);
}
});
} }
/// <summary> /// <summary>
@@ -63,12 +68,14 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception> /// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool Remove(string name) public static bool Remove(string name)
{ {
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); return Utf8Marshaller.WithUtf8(name, namePtr =>
fixed (byte* namePtr = nameBytes)
{ {
var result = Internal.API.regorus_target_registry_remove(namePtr); unsafe
return GetBoolResult(result); {
} var result = Internal.API.regorus_target_registry_remove((byte*)namePtr);
return GetBoolResult(result);
}
});
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,154 @@
// 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);
}
}
}