diff --git a/bindings/csharp/Benchmarks/CompiledPolicyEvaluationBenchmark.cs b/bindings/csharp/Benchmarks/CompiledPolicyEvaluationBenchmark.cs index 75be000..e15b49c 100644 --- a/bindings/csharp/Benchmarks/CompiledPolicyEvaluationBenchmark.cs +++ b/bindings/csharp/Benchmarks/CompiledPolicyEvaluationBenchmark.cs @@ -129,12 +129,12 @@ namespace Benchmarks Console.WriteLine($"Warming up with {threads} threads for {warmupSeconds} seconds..."); // 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..."); // 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) var evalsPerSecond = totalEvaluations / evaluationTime.TotalSeconds; @@ -144,6 +144,12 @@ namespace Benchmarks Console.WriteLine($" time: [{evaluationTime.TotalMilliseconds:F2} ms]"); 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 if (compiledPolicies != null) { @@ -166,7 +172,7 @@ namespace Benchmarks } } - private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary policyCounters) RunBenchmarkPhase( + private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary policyCounters, long allocatedBytes) RunBenchmarkPhase( int threads, int durationSeconds, List<(string Policy, string[] Inputs)> policiesWithInputs, @@ -180,6 +186,7 @@ namespace Benchmarks var evaluationTimes = new Dictionary(); var lockObject = new object(); var stopExecution = false; + long allocatedBytes = 0; // Initialize counters foreach (var policyName in PolicyNames) @@ -194,6 +201,12 @@ namespace Benchmarks int tid = threadId; tasks[threadId] = Task.Run(() => { + long allocationStart = 0; + if (!isWarmup) + { + allocationStart = GC.GetAllocatedBytesForCurrentThread(); + } + barrier.SignalAndWait(); int evaluationCount = 0; @@ -256,6 +269,9 @@ namespace Benchmarks evaluationTimes[tid] = TimeSpan.Zero; 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) var evaluationTime = totalEvaluationTime == TimeSpan.Zero ? stopwatch.Elapsed : totalEvaluationTime; - return (totalEvaluations, evaluationTime, policyCounters); + return (totalEvaluations, evaluationTime, policyCounters, allocatedBytes); } } } diff --git a/bindings/csharp/Regorus.Tests/RegorusTests.cs b/bindings/csharp/Regorus.Tests/RegorusTests.cs index b951314..e148091 100644 --- a/bindings/csharp/Regorus.Tests/RegorusTests.cs +++ b/bindings/csharp/Regorus.Tests/RegorusTests.cs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Regorus.Tests; - -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; using System.Text.Json.Nodes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Regorus; + +namespace Regorus.Tests; [TestClass] public class RegorusTests @@ -212,4 +214,43 @@ public class RegorusTests Assert.AreEqual("a", parameters![0]["parameters"][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)." + ); + } } \ No newline at end of file diff --git a/bindings/csharp/Regorus/CompiledPolicy.cs b/bindings/csharp/Regorus/CompiledPolicy.cs index 46aaebe..266afac 100644 --- a/bindings/csharp/Regorus/CompiledPolicy.cs +++ b/bindings/csharp/Regorus/CompiledPolicy.cs @@ -4,6 +4,7 @@ using System; using System.Text; using System.Text.Json; +using System.Threading; #nullable enable namespace Regorus @@ -23,13 +24,14 @@ namespace Regorus /// public unsafe sealed class CompiledPolicy : IDisposable { - private Internal.RegorusCompiledPolicy* _policy; - private int _isDisposed; - private int _activeEvaluations; + private RegorusCompiledPolicyHandle? _handle; + private readonly ManualResetEventSlim _idleEvent = new(initialState: true); + private int _isDisposed; + private int _activeEvaluations; - internal CompiledPolicy(Internal.RegorusCompiledPolicy* policy) + internal CompiledPolicy(RegorusCompiledPolicyHandle handle) { - _policy = policy; + _handle = handle ?? throw new ArgumentNullException(nameof(handle)); } /// @@ -44,21 +46,34 @@ namespace Regorus public string? EvalWithInput(string inputJson) { // Increment active evaluations count - System.Threading.Interlocked.Increment(ref _activeEvaluations); + var active = System.Threading.Interlocked.Increment(ref _activeEvaluations); + if (active == 1) + { + _idleEvent.Reset(); + } try { ThrowIfDisposed(); - - var inputBytes = Encoding.UTF8.GetBytes(inputJson + char.MinValue); - fixed (byte* inputPtr = inputBytes) + + return Internal.Utf8Marshaller.WithUtf8(inputJson, inputPtr => { - 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 { // 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() { 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)) { @@ -105,25 +126,22 @@ namespace Regorus { 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 - while (System.Threading.Volatile.Read(ref _activeEvaluations) > 0) - { - System.Threading.Thread.Yield(); - } + _idleEvent.Wait(); - Internal.API.regorus_compiled_policy_drop(_policy); - _policy = null; + handle.Dispose(); + _handle = null; } + + _idleEvent.Dispose(); } } - ~CompiledPolicy() => Dispose(disposing: false); - private void ThrowIfDisposed() { - if (_isDisposed != 0) + if (_isDisposed != 0 || _handle is null || _handle.IsClosed) throw new ObjectDisposedException(nameof(CompiledPolicy)); } @@ -164,5 +182,39 @@ namespace Regorus 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(Func 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(); + } + } + } } } diff --git a/bindings/csharp/Regorus/Compiler.cs b/bindings/csharp/Regorus/Compiler.cs index aa27a5c..3870bac 100644 --- a/bindings/csharp/Regorus/Compiler.cs +++ b/bindings/csharp/Regorus/Compiler.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; +using Regorus.Internal; #nullable enable namespace Regorus @@ -54,49 +55,48 @@ namespace Regorus /// Thrown when compilation fails public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IEnumerable modules, string entryPointRule) { - var dataBytes = Encoding.UTF8.GetBytes(dataJson + char.MinValue); - var entryPointBytes = Encoding.UTF8.GetBytes(entryPointRule + char.MinValue); var modulesArray = modules.ToArray(); - // Convert C# modules to native structs var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length]; - var pinnedHandles = new List(); + var pinnedStrings = new List(modulesArray.Length * 2); try { for (int i = 0; i < modulesArray.Length; i++) { - var idBytes = Encoding.UTF8.GetBytes(modulesArray[i].Id + char.MinValue); - var contentBytes = Encoding.UTF8.GetBytes(modulesArray[i].Content + char.MinValue); - - var idHandle = GCHandle.Alloc(idBytes, GCHandleType.Pinned); - var contentHandle = GCHandle.Alloc(contentBytes, GCHandleType.Pinned); - pinnedHandles.Add(idHandle); - pinnedHandles.Add(contentHandle); + var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id); + var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content); + pinnedStrings.Add(idPinned); + pinnedStrings.Add(contentPinned); nativeModules[i] = new Internal.RegorusPolicyModule { - id = (byte*)idHandle.AddrOfPinnedObject(), - content = (byte*)contentHandle.AddrOfPinnedObject() + id = idPinned.Pointer, + content = contentPinned.Pointer }; } - fixed (byte* dataPtr = dataBytes) - fixed (byte* entryPointPtr = entryPointBytes) - fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules) - { - var result = Internal.API.regorus_compile_policy_with_entrypoint( - dataPtr, modulesPtr, (UIntPtr)modulesArray.Length, entryPointPtr); + return Utf8Marshaller.WithUtf8(dataJson, dataPtr => + Utf8Marshaller.WithUtf8(entryPointRule, entryPointPtr => + { + unsafe + { + 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); - return policy; - } + var policy = GetCompiledPolicyResult(result); + return policy; + } + } + })); } finally { - foreach (var handle in pinnedHandles) + foreach (var pinned in pinnedStrings) { - handle.Free(); + pinned.Dispose(); } } } @@ -112,47 +112,47 @@ namespace Regorus /// Thrown when compilation fails public static CompiledPolicy CompilePolicyForTarget(string dataJson, IEnumerable modules) { - var dataBytes = Encoding.UTF8.GetBytes(dataJson + char.MinValue); var modulesArray = modules.ToArray(); - // Convert C# modules to native structs var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length]; - var pinnedHandles = new List(); + var pinnedStrings = new List(modulesArray.Length * 2); try { for (int i = 0; i < modulesArray.Length; i++) { - var idBytes = Encoding.UTF8.GetBytes(modulesArray[i].Id + char.MinValue); - var contentBytes = Encoding.UTF8.GetBytes(modulesArray[i].Content + char.MinValue); - - var idHandle = GCHandle.Alloc(idBytes, GCHandleType.Pinned); - var contentHandle = GCHandle.Alloc(contentBytes, GCHandleType.Pinned); - pinnedHandles.Add(idHandle); - pinnedHandles.Add(contentHandle); + var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id); + var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content); + pinnedStrings.Add(idPinned); + pinnedStrings.Add(contentPinned); nativeModules[i] = new Internal.RegorusPolicyModule { - id = (byte*)idHandle.AddrOfPinnedObject(), - content = (byte*)contentHandle.AddrOfPinnedObject() + id = idPinned.Pointer, + content = contentPinned.Pointer }; } - fixed (byte* dataPtr = dataBytes) - fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules) + return Utf8Marshaller.WithUtf8(dataJson, dataPtr => { - var result = Internal.API.regorus_compile_policy_for_target( - dataPtr, modulesPtr, (UIntPtr)modulesArray.Length); + unsafe + { + fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules) + { + var result = Internal.API.regorus_compile_policy_for_target( + (byte*)dataPtr, modulesPtr, (UIntPtr)modulesArray.Length); - var policy = GetCompiledPolicyResult(result); - return policy; - } + var policy = GetCompiledPolicyResult(result); + return policy; + } + } + }); } 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"); } - return new CompiledPolicy((Internal.RegorusCompiledPolicy*)result.pointer_value); + var handle = RegorusCompiledPolicyHandle.FromPointer((IntPtr)result.pointer_value); + return new CompiledPolicy(handle); } finally { diff --git a/bindings/csharp/Regorus/Engine.cs b/bindings/csharp/Regorus/Engine.cs index 4773d4c..2d8ed11 100644 --- a/bindings/csharp/Regorus/Engine.cs +++ b/bindings/csharp/Regorus/Engine.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.InteropServices; using System.Text; +using Regorus.Internal; #nullable enable @@ -15,17 +16,14 @@ namespace Regorus /// 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. /// - public unsafe sealed class Engine : System.IDisposable + public unsafe sealed class Engine : IDisposable { - private Regorus.Internal.RegorusEngine* E; - // Detect redundant Dispose() calls in a thread-safe manner. - // _isDisposed == 0 means Dispose(bool) has not been called yet. - // _isDisposed == 1 means Dispose(bool) has been already called. - private int isDisposed; + private RegorusEngineHandle? _handle; + private int _isDisposed; public Engine() { - E = Regorus.Internal.API.regorus_engine_new(); + _handle = RegorusEngineHandle.Create(); } public void Dispose() @@ -49,182 +47,314 @@ namespace Regorus // other objects. Only unmanaged resources can be disposed. void Dispose(bool disposing) { - // In case _isDisposed is 0, atomically set it to 1. - // Enter the branch only if the original value is 0. - if (System.Threading.Interlocked.CompareExchange(ref isDisposed, 1, 0) == 0) + if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) { - // If disposing equals true, dispose all managed - // and unmanaged resources. - 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; - } - + _handle?.Dispose(); + _handle = null; } } - // Use C# finalizer syntax for finalization code. - // 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) + private Engine(RegorusEngineHandle handle) { - 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) { - 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) { - var pathBytes = NullTerminatedUTF8Bytes(path); - var regoBytes = NullTerminatedUTF8Bytes(rego); - - - fixed (byte* pathPtr = pathBytes) - { - fixed (byte* regoPtr = regoBytes) + ThrowIfDisposed(); + return Utf8Marshaller.WithUtf8(path, pathPtr => + Utf8Marshaller.WithUtf8(rego, regoPtr => { - 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) { - 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) { - var pathBytes = NullTerminatedUTF8Bytes(path); - fixed (byte* pathPtr = pathBytes) + ThrowIfDisposed(); + 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) { - var dataBytes = NullTerminatedUTF8Bytes(data); - fixed (byte* dataPtr = dataBytes) + ThrowIfDisposed(); + 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) { - var pathBytes = NullTerminatedUTF8Bytes(path); - fixed (byte* pathPtr = pathBytes) + ThrowIfDisposed(); + 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) { - var inputBytes = NullTerminatedUTF8Bytes(input); - fixed (byte* inputPtr = inputBytes) + ThrowIfDisposed(); + 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) { - var pathBytes = NullTerminatedUTF8Bytes(path); - fixed (byte* pathPtr = pathBytes) + ThrowIfDisposed(); + 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) { - var queryBytes = NullTerminatedUTF8Bytes(query); - fixed (byte* queryPtr = queryBytes) + ThrowIfDisposed(); + 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) { - var ruleBytes = NullTerminatedUTF8Bytes(rule); - fixed (byte* rulePtr = ruleBytes) + ThrowIfDisposed(); + 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) { - 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() { - 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() { - 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() { - 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) { - 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() { - 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() { - 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() { - 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() { - 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) @@ -260,5 +390,56 @@ namespace Regorus 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 action) + { + UseHandle(handlePtr => + { + action(handlePtr); + return null; + }); + } + + private T UseHandle(Func 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(); + } + } + } + } } diff --git a/bindings/csharp/Regorus/SafeHandles.cs b/bindings/csharp/Regorus/SafeHandles.cs new file mode 100644 index 0000000..4fbcf17 --- /dev/null +++ b/bindings/csharp/Regorus/SafeHandles.cs @@ -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; + } + } +} diff --git a/bindings/csharp/Regorus/SchemaRegistry.cs b/bindings/csharp/Regorus/SchemaRegistry.cs index 2c061dc..6ef8d87 100644 --- a/bindings/csharp/Regorus/SchemaRegistry.cs +++ b/bindings/csharp/Regorus/SchemaRegistry.cs @@ -3,6 +3,7 @@ using System; using System.Text; +using Regorus.Internal; #nullable enable namespace Regorus @@ -21,14 +22,16 @@ namespace Regorus /// Thrown when schema registration fails public static void RegisterResource(string name, string schemaJson) { - var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); - var schemaBytes = Encoding.UTF8.GetBytes(schemaJson + char.MinValue); - - fixed (byte* namePtr = nameBytes) - fixed (byte* schemaPtr = schemaBytes) + Utf8Marshaller.WithUtf8(name, namePtr => { - 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)); + } + }); + }); } /// @@ -39,12 +42,14 @@ namespace Regorus /// Thrown when the operation fails public static bool ContainsResource(string name) { - var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); - fixed (byte* namePtr = nameBytes) + return Utf8Marshaller.WithUtf8(name, namePtr => { - var result = Internal.API.regorus_resource_schema_contains(namePtr); - return GetBoolResult(result); - } + unsafe + { + var result = Internal.API.regorus_resource_schema_contains((byte*)namePtr); + return GetBoolResult(result); + } + }); } /// @@ -93,12 +98,14 @@ namespace Regorus /// Thrown when the operation fails public static bool RemoveResource(string name) { - var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); - fixed (byte* namePtr = nameBytes) + return Utf8Marshaller.WithUtf8(name, namePtr => { - var result = Internal.API.regorus_resource_schema_remove(namePtr); - return GetBoolResult(result); - } + unsafe + { + var result = Internal.API.regorus_resource_schema_remove((byte*)namePtr); + return GetBoolResult(result); + } + }); } /// @@ -118,14 +125,16 @@ namespace Regorus /// Thrown when schema registration fails public static void RegisterEffect(string name, string schemaJson) { - var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); - var schemaBytes = Encoding.UTF8.GetBytes(schemaJson + char.MinValue); - - fixed (byte* namePtr = nameBytes) - fixed (byte* schemaPtr = schemaBytes) + Utf8Marshaller.WithUtf8(name, namePtr => { - 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)); + } + }); + }); } /// @@ -136,12 +145,14 @@ namespace Regorus /// Thrown when the operation fails public static bool ContainsEffect(string name) { - var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); - fixed (byte* namePtr = nameBytes) + return Utf8Marshaller.WithUtf8(name, namePtr => { - var result = Internal.API.regorus_effect_schema_contains(namePtr); - return GetBoolResult(result); - } + unsafe + { + var result = Internal.API.regorus_effect_schema_contains((byte*)namePtr); + return GetBoolResult(result); + } + }); } /// @@ -190,12 +201,14 @@ namespace Regorus /// Thrown when the operation fails public static bool RemoveEffect(string name) { - var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); - fixed (byte* namePtr = nameBytes) + return Utf8Marshaller.WithUtf8(name, namePtr => { - var result = Internal.API.regorus_effect_schema_remove(namePtr); - return GetBoolResult(result); - } + unsafe + { + var result = Internal.API.regorus_effect_schema_remove((byte*)namePtr); + return GetBoolResult(result); + } + }); } /// diff --git a/bindings/csharp/Regorus/TargetRegistry.cs b/bindings/csharp/Regorus/TargetRegistry.cs index 25ee107..e2df083 100644 --- a/bindings/csharp/Regorus/TargetRegistry.cs +++ b/bindings/csharp/Regorus/TargetRegistry.cs @@ -3,6 +3,7 @@ using System; using System.Text; +using Regorus.Internal; #nullable enable namespace Regorus @@ -22,11 +23,13 @@ namespace Regorus /// Thrown when target registration fails public static void RegisterFromJson(string targetJson) { - var targetBytes = Encoding.UTF8.GetBytes(targetJson + char.MinValue); - fixed (byte* targetPtr = targetBytes) + Utf8Marshaller.WithUtf8(targetJson, targetPtr => { - CheckAndDropResult(Internal.API.regorus_register_target_from_json(targetPtr)); - } + unsafe + { + CheckAndDropResult(Internal.API.regorus_register_target_from_json((byte*)targetPtr)); + } + }); } /// @@ -37,12 +40,14 @@ namespace Regorus /// Thrown when the operation fails public static bool Contains(string name) { - var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); - fixed (byte* namePtr = nameBytes) + return Utf8Marshaller.WithUtf8(name, namePtr => { - var result = Internal.API.regorus_target_registry_contains(namePtr); - return GetBoolResult(result); - } + unsafe + { + var result = Internal.API.regorus_target_registry_contains((byte*)namePtr); + return GetBoolResult(result); + } + }); } /// @@ -63,12 +68,14 @@ namespace Regorus /// Thrown when the operation fails public static bool Remove(string name) { - var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue); - fixed (byte* namePtr = nameBytes) + return Utf8Marshaller.WithUtf8(name, namePtr => { - var result = Internal.API.regorus_target_registry_remove(namePtr); - return GetBoolResult(result); - } + unsafe + { + var result = Internal.API.regorus_target_registry_remove((byte*)namePtr); + return GetBoolResult(result); + } + }); } /// diff --git a/bindings/csharp/Regorus/Utf8Marshaller.cs b/bindings/csharp/Regorus/Utf8Marshaller.cs new file mode 100644 index 0000000..a70872c --- /dev/null +++ b/bindings/csharp/Regorus/Utf8Marshaller.cs @@ -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 +{ + /// + /// 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. + /// + 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; + + /// + /// 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). + /// + 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.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.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.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(value, ptr => + { + action((byte*)ptr); + return null; + }); + } + + internal static T WithUtf8(string value, Func 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 buffer = stackalloc byte[required]; + return Invoke(value, func, buffer, byteCount); + } + + var rented = ArrayPool.Shared.Rent(required); + try + { + Span buffer = rented; + return Invoke(value, func, buffer, byteCount); + } + finally + { + ArrayPool.Shared.Return(rented); + } + } + + private static unsafe T Invoke(string value, Func func, Span 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); + } + } +}