From 96360fa9d8e54c5890aa6b63274da6a64b7d3b34 Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Mon, 9 Feb 2026 23:52:05 +0530 Subject: [PATCH] fix(bindings): add SafeHandleWrapper + memory growth checks; bump 0.9.1 (#571) - Introduce SafeHandleWrapper with gating, short drain wait, and deferred release on last in-flight exit. - Wire Engine/Program/Rvm/CompiledPolicy to wrapper (centralized handle use, interop helper). - Add C# memory growth tests (using/finalizer paths) and extend xtask C# runner options. - Add pooled marshalling utilities, ResultHelpers, and API cleanups; update versions/changelog. Fixes #570. Closes #554 Signed-off-by: Anand Krishnamoorthi --- CHANGELOG.md | 10 + Cargo.lock | 2 +- Cargo.toml | 2 +- .../CompiledPolicyEvaluationBenchmark.cs | 82 +++-- .../Benchmarks/EngineEvaluationBenchmark.cs | 56 +-- bindings/csharp/Benchmarks/Program.cs | 8 +- bindings/csharp/Directory.Packages.props | 2 +- .../csharp/Regorus.Tests/MemoryGrowthTests.cs | 320 +++++++++++++++++ bindings/csharp/Regorus.Tests/RegorusTests.cs | 332 +++++++++++------- .../csharp/Regorus.Tests/RvmProgramTests.cs | 8 +- bindings/csharp/Regorus/CompiledPolicy.cs | 154 +------- bindings/csharp/Regorus/Compiler.cs | 140 ++++---- bindings/csharp/Regorus/Engine.cs | 322 +++-------------- bindings/csharp/Regorus/MemoryLimits.cs | 10 +- bindings/csharp/Regorus/ModuleMarshalling.cs | 156 ++++++++ bindings/csharp/Regorus/Program.cs | 240 ++++--------- bindings/csharp/Regorus/Regorus.csproj | 2 +- bindings/csharp/Regorus/ResultHelpers.cs | 73 ++++ bindings/csharp/Regorus/Rvm.cs | 123 ++----- bindings/csharp/Regorus/SafeHandleWrapper.cs | 272 ++++++++++++++ bindings/csharp/Regorus/SafeHandles.cs | 8 +- bindings/csharp/Regorus/SchemaRegistry.cs | 109 ++---- bindings/csharp/Regorus/TargetRegistry.cs | 87 +---- bindings/csharp/Regorus/Utf8Marshaller.cs | 8 +- bindings/csharp/TargetExampleApp/Program.cs | 54 +-- bindings/csharp/TestApp/Program.cs | 3 +- bindings/ffi/Cargo.lock | 4 +- bindings/ffi/Cargo.toml | 2 +- bindings/java/Cargo.lock | 4 +- bindings/java/Cargo.toml | 2 +- bindings/java/pom.xml | 2 +- bindings/python/Cargo.lock | 4 +- bindings/python/Cargo.toml | 2 +- bindings/ruby/ext/regorusrb/Cargo.toml | 2 +- bindings/ruby/lib/regorus/version.rb | 2 +- bindings/wasm/Cargo.lock | 4 +- bindings/wasm/Cargo.toml | 2 +- xtask/src/tasks/bindings/all.rs | 3 + xtask/src/tasks/bindings/csharp.rs | 31 ++ 39 files changed, 1496 insertions(+), 1151 deletions(-) create mode 100644 bindings/csharp/Regorus.Tests/MemoryGrowthTests.cs create mode 100644 bindings/csharp/Regorus/ModuleMarshalling.cs create mode 100644 bindings/csharp/Regorus/ResultHelpers.cs create mode 100644 bindings/csharp/Regorus/SafeHandleWrapper.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index fd047e0..5df91d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.1](https://github.com/microsoft/regorus/compare/regorus-v0.9.0...regorus-v0.9.1) - 2026-02-06 + +### Fixed +- Release native C# handles reliably to avoid memory growth ([#571](https://github.com/microsoft/regorus/pull/571)). +- Centralize C# handle gating with a short dispose wait and deferred release to avoid leaks while blocking new calls ([#571](https://github.com/microsoft/regorus/pull/571)). + +### Added +- Manual C# memory growth tests for both `using` and finalizer paths ([#571](https://github.com/microsoft/regorus/pull/571)). +- C# test runner options for filtered tests, console logging, and skipping sample apps ([#571](https://github.com/microsoft/regorus/pull/571)). + ## [0.5.0](https://github.com/microsoft/regorus/compare/regorus-v0.4.0...regorus-v0.5.0) - 2025-07-08 ### Added diff --git a/Cargo.lock b/Cargo.lock index fddf06b..9eb995c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1227,7 +1227,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "regorus" -version = "0.9.0" +version = "0.9.1" dependencies = [ "anyhow", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 45f5a80..400250c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ [package] name = "regorus" description = "A fast, lightweight Rego (OPA policy language) interpreter" -version = "0.9.0" +version = "0.9.1" edition = "2021" license = "MIT AND Apache-2.0 AND BSD-3-Clause" repository = "https://github.com/microsoft/regorus" diff --git a/bindings/csharp/Benchmarks/CompiledPolicyEvaluationBenchmark.cs b/bindings/csharp/Benchmarks/CompiledPolicyEvaluationBenchmark.cs index e15b49c..a1e827f 100644 --- a/bindings/csharp/Benchmarks/CompiledPolicyEvaluationBenchmark.cs +++ b/bindings/csharp/Benchmarks/CompiledPolicyEvaluationBenchmark.cs @@ -12,7 +12,7 @@ namespace Benchmarks public class CompiledPolicyEvaluationBenchmark { private static readonly string TestDataPath = Path.Combine( - Directory.GetCurrentDirectory(), + Directory.GetCurrentDirectory(), "..", "..", "..", "benches", "evaluation", "test_data" ); @@ -33,7 +33,7 @@ namespace Benchmarks private static readonly string[] PolicyNames = new[] { "rbac_policy", - "api_access_policy", + "api_access_policy", "data_sensitivity_policy", "time_based_policy", "data_processing_policy", @@ -46,21 +46,21 @@ namespace Benchmarks private static List<(string Policy, string[] Inputs)> LoadPoliciesWithInputs() { var result = new List<(string Policy, string[] Inputs)>(); - + foreach (var (policyFile, inputFiles) in PolicyInputFiles) { var policyPath = Path.Combine(TestDataPath, "policies", policyFile); var policy = File.ReadAllText(policyPath); - + var inputs = inputFiles.Select(inputFile => { var inputPath = Path.Combine(TestDataPath, "inputs", inputFile); return File.ReadAllText(inputPath); }).ToArray(); - + result.Add((policy, inputs)); } - + return result; } @@ -68,14 +68,14 @@ namespace Benchmarks { var policiesWithInputs = LoadPoliciesWithInputs(); var compiledPolicies = new List(); - + foreach (var (policy, _) in policiesWithInputs) { - var modules = new[] { new PolicyModule { Id = "policy.rego", Content = policy } }; + var modules = new[] { new PolicyModule("policy.rego", policy) }; var compiled = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.bench.allow"); compiledPolicies.Add(compiled); } - + return compiledPolicies; } @@ -84,13 +84,13 @@ namespace Benchmarks var cpuCount = Environment.ProcessorCount; var maxThreads = cpuCount * 2; var threadCounts = new List { 1, 2 }; - + // Add even numbers from 4 to maxThreads for (int i = 4; i <= maxThreads; i += 2) { threadCounts.Add(i); } - + Console.WriteLine($"Running compiled policy benchmark with max_threads: {maxThreads}"); Console.WriteLine($"Testing with thread counts: {string.Join(", ", threadCounts)}"); Console.WriteLine(); @@ -120,19 +120,19 @@ namespace Benchmarks const int durationSeconds = 3; var policiesWithInputs = LoadPoliciesWithInputs(); List? compiledPolicies = null; - + if (useSharedPolicies) { compiledPolicies = PrepareSharedCompiledPolicies(); } - + Console.WriteLine($"Warming up with {threads} threads for {warmupSeconds} seconds..."); - + // Warmup phase 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, allocatedBytes) = RunBenchmarkPhase(threads, durationSeconds, policiesWithInputs, compiledPolicies, useSharedPolicies, isWarmup: false); @@ -155,7 +155,7 @@ namespace Benchmarks { foreach (var policy in compiledPolicies) { - policy.Dispose(); + DisposeCompiledPolicy(policy); } } @@ -173,8 +173,8 @@ namespace Benchmarks } private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary policyCounters, long allocatedBytes) RunBenchmarkPhase( - int threads, - int durationSeconds, + int threads, + int durationSeconds, List<(string Policy, string[] Inputs)> policiesWithInputs, List? compiledPolicies, bool useSharedPolicies, @@ -208,10 +208,10 @@ namespace Benchmarks } barrier.SignalAndWait(); - + int evaluationCount = 0; var localEvaluationTime = TimeSpan.Zero; - + while (!stopExecution) { // Use different policy for each iteration @@ -226,23 +226,29 @@ namespace Benchmarks { // Measure only the evaluation call var evalStopwatch = Stopwatch.StartNew(); - + if (useSharedPolicies) { var result = compiledPolicies![policyIdx].EvalWithInput(input); } else { - // Compile policy in each iteration - var modules = new[] { new PolicyModule { Id = "policy.rego", Content = policy } }; + // Compile policy in each iteration. + var modules = new[] { new PolicyModule("policy.rego", policy) }; var compiled = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.bench.allow"); - var result = compiled.EvalWithInput(input); - compiled.Dispose(); + try + { + var result = compiled.EvalWithInput(input); + } + finally + { + DisposeCompiledPolicy(compiled); + } } - + evalStopwatch.Stop(); localEvaluationTime += evalStopwatch.Elapsed; - + // Track successful evaluations (only during actual benchmark, not warmup) if (!isWarmup) { @@ -256,10 +262,10 @@ namespace Benchmarks { // Ignore evaluation errors for benchmarking purposes } - + evaluationCount++; } - + // Store the actual evaluation time for this thread if (!isWarmup) { @@ -284,11 +290,23 @@ namespace Benchmarks var totalEvaluations = policyCounters.Values.Sum(); var totalEvaluationTime = evaluationTimes.Values.Aggregate(TimeSpan.Zero, (sum, time) => sum + time); - + // Use pure evaluation time (consistent with Rust benchmark) var evaluationTime = totalEvaluationTime == TimeSpan.Zero ? stopwatch.Elapsed : totalEvaluationTime; - + return (totalEvaluations, evaluationTime, policyCounters, allocatedBytes); } + + private static void DisposeCompiledPolicy(CompiledPolicy policy) + { + try + { + policy.Dispose(); + } + catch (TimeoutException ex) + { + Console.WriteLine($"Warning: {ex.Message}"); + } + } } } diff --git a/bindings/csharp/Benchmarks/EngineEvaluationBenchmark.cs b/bindings/csharp/Benchmarks/EngineEvaluationBenchmark.cs index 2db8a13..b53b033 100644 --- a/bindings/csharp/Benchmarks/EngineEvaluationBenchmark.cs +++ b/bindings/csharp/Benchmarks/EngineEvaluationBenchmark.cs @@ -12,7 +12,7 @@ namespace Benchmarks public class EngineEvaluationBenchmark { private static readonly string TestDataPath = Path.Combine( - Directory.GetCurrentDirectory(), + Directory.GetCurrentDirectory(), "..", "..", "..", "benches", "evaluation", "test_data" ); @@ -33,7 +33,7 @@ namespace Benchmarks private static readonly string[] PolicyNames = new[] { "rbac_policy", - "api_access_policy", + "api_access_policy", "data_sensitivity_policy", "time_based_policy", "data_processing_policy", @@ -46,21 +46,21 @@ namespace Benchmarks private static List<(string Policy, string[] Inputs)> LoadPoliciesWithInputs() { var result = new List<(string Policy, string[] Inputs)>(); - + foreach (var (policyFile, inputFiles) in PolicyInputFiles) { var policyPath = Path.Combine(TestDataPath, "policies", policyFile); var policy = File.ReadAllText(policyPath); - + var inputs = inputFiles.Select(inputFile => { var inputPath = Path.Combine(TestDataPath, "inputs", inputFile); return File.ReadAllText(inputPath); }).ToArray(); - + result.Add((policy, inputs)); } - + return result; } @@ -68,12 +68,12 @@ namespace Benchmarks { var policiesWithInputs = LoadPoliciesWithInputs(); var engines = new List(); - + foreach (var (policy, _) in policiesWithInputs) { var engine = new Engine(); engine.AddPolicy("policy.rego", policy); - + // Warm up the engine to ensure it's fully prepared for evaluation // This prevents each cloned engine from repeating preparation work engine.SetInputJson("{}"); @@ -85,10 +85,10 @@ namespace Benchmarks { // Ignore warmup errors } - + engines.Add(engine); } - + return engines; } @@ -97,13 +97,13 @@ namespace Benchmarks var cpuCount = Environment.ProcessorCount; var maxThreads = cpuCount * 2; var threadCounts = new List { 1, 2 }; - + // Add even numbers from 4 to maxThreads for (int i = 4; i <= maxThreads; i += 2) { threadCounts.Add(i); } - + Console.WriteLine($"Running engine benchmark with max_threads: {maxThreads}"); Console.WriteLine($"Testing with thread counts: {string.Join(", ", threadCounts)}"); Console.WriteLine(); @@ -132,14 +132,14 @@ namespace Benchmarks const int warmupSeconds = 3; const int durationSeconds = 3; var policiesWithInputs = LoadPoliciesWithInputs(); - + Console.WriteLine($"Warming up with {threads} threads for {warmupSeconds} seconds..."); - + // Warmup phase var (_, _, _) = RunBenchmarkPhase(threads, warmupSeconds, policiesWithInputs, useClonedEngines, isWarmup: true); - + Console.WriteLine($"Running benchmark with {threads} threads for {durationSeconds} seconds..."); - + // Actual benchmark phase var (totalEvaluations, evaluationTime, policyCounters) = RunBenchmarkPhase(threads, durationSeconds, policiesWithInputs, useClonedEngines, isWarmup: false); @@ -165,8 +165,8 @@ namespace Benchmarks } private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary policyCounters) RunBenchmarkPhase( - int threads, - int durationSeconds, + int threads, + int durationSeconds, List<(string Policy, string[] Inputs)> policiesWithInputs, bool useClonedEngines, bool isWarmup) @@ -199,10 +199,10 @@ namespace Benchmarks tasks[threadId] = Task.Run(() => { barrier.SignalAndWait(); - + int evaluationCount = 0; var localEvaluationTime = TimeSpan.Zero; - + while (!stopExecution) { // Use different policy for each iteration @@ -217,7 +217,7 @@ namespace Benchmarks { // Measure only the engine operations var evalStopwatch = Stopwatch.StartNew(); - + Engine engine; if (useClonedEngines) { @@ -228,14 +228,14 @@ namespace Benchmarks engine = new Engine(); engine.AddPolicy("policy.rego", policy); } - + engine.SetInputJson(input); var result = engine.EvalRule("data.bench.allow"); engine.Dispose(); - + evalStopwatch.Stop(); localEvaluationTime += evalStopwatch.Elapsed; - + // Track successful evaluations (only during actual benchmark, not warmup) if (!isWarmup) { @@ -249,10 +249,10 @@ namespace Benchmarks { // Ignore evaluation errors for benchmarking purposes } - + evaluationCount++; } - + // Store the actual evaluation time for this thread if (!isWarmup) { @@ -283,10 +283,10 @@ namespace Benchmarks var totalEvaluations = policyCounters.Values.Sum(); var totalEvaluationTime = evaluationTimes.Values.Aggregate(TimeSpan.Zero, (sum, time) => sum + time); - + // Use pure evaluation time (consistent with Rust benchmark) var evaluationTime = totalEvaluationTime == TimeSpan.Zero ? stopwatch.Elapsed : totalEvaluationTime; - + return (totalEvaluations, evaluationTime, policyCounters); } } diff --git a/bindings/csharp/Benchmarks/Program.cs b/bindings/csharp/Benchmarks/Program.cs index ef15f1e..f7bf812 100644 --- a/bindings/csharp/Benchmarks/Program.cs +++ b/bindings/csharp/Benchmarks/Program.cs @@ -7,7 +7,7 @@ namespace Benchmarks static void Main(string[] args) { Console.WriteLine("=== Regorus C# Benchmarks ===\n"); - + try { Console.WriteLine("Running Engine Evaluation Benchmark..."); @@ -17,9 +17,9 @@ namespace Benchmarks { Console.WriteLine($"Engine benchmark failed: {ex.Message}"); } - + Console.WriteLine("\n" + new string('=', 80) + "\n"); - + try { Console.WriteLine("Running Compiled Policy Evaluation Benchmark..."); @@ -29,7 +29,7 @@ namespace Benchmarks { Console.WriteLine($"Compiled policy benchmark failed: {ex.Message}"); } - + Console.WriteLine("\n=== Benchmarks Complete ==="); } } diff --git a/bindings/csharp/Directory.Packages.props b/bindings/csharp/Directory.Packages.props index 26741f4..6cb753e 100644 --- a/bindings/csharp/Directory.Packages.props +++ b/bindings/csharp/Directory.Packages.props @@ -1,7 +1,7 @@ true - 0.9.0 + 0.9.1 -$(VersionSuffix) diff --git a/bindings/csharp/Regorus.Tests/MemoryGrowthTests.cs b/bindings/csharp/Regorus.Tests/MemoryGrowthTests.cs new file mode 100644 index 0000000..e45331c --- /dev/null +++ b/bindings/csharp/Regorus.Tests/MemoryGrowthTests.cs @@ -0,0 +1,320 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Regorus; + +namespace Regorus.Tests; + +[TestClass] +[DoNotParallelize] +public class MemoryGrowthTests +{ + private static int Iterations => + int.TryParse(Environment.GetEnvironmentVariable("REGORUS_MEMORY_TEST_ITERS"), out var value) ? value : 50_000; + + private static int LogEvery => + int.TryParse(Environment.GetEnvironmentVariable("REGORUS_MEMORY_TEST_LOG_EVERY"), out var value) ? value : 500; + + private static int GcEvery + { + get + { + if (!int.TryParse(Environment.GetEnvironmentVariable("REGORUS_MEMORY_TEST_GC_EVERY"), out var value)) + { + value = LogEvery; + } + + return value <= 0 ? LogEvery : value; + } + } + + private static long? MaxWorkingSetDeltaBytes + { + get + { + if (!long.TryParse(Environment.GetEnvironmentVariable("REGORUS_MEMORY_TEST_MAX_DELTA_MB"), out var mb)) + { + mb = 32; + } + + if (mb <= 0) + { + return null; + } + + return mb * 1024L * 1024L; + } + } + + private static ulong? GlobalRegorusMemoryLimitBytes + { + get + { + if (!ulong.TryParse(Environment.GetEnvironmentVariable("REGORUS_MEMORY_TEST_GLOBAL_REGORUS_LIMIT_MB"), out var mb)) + { + return null; + } + + if (mb == 0) + { + return null; + } + + return mb * 1024UL * 1024UL; + } + } + + private static void WithOptionalGlobalRegorusMemoryLimit(Action action) + { + var priorLimit = MemoryLimits.GetGlobalMemoryLimit(); + try + { + if (GlobalRegorusMemoryLimitBytes is { } limit) + { + MemoryLimits.SetGlobalMemoryLimit(limit); + } + + action(); + } + finally + { + MemoryLimits.SetGlobalMemoryLimit(priorLimit); + } + } + + private static void ForceFullGc() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + + + [TestMethod] + public void Engine_create_eval_dispose_does_not_grow_working_set() + { + WithOptionalGlobalRegorusMemoryLimit(() => + { + var process = Process.GetCurrentProcess(); + process.Refresh(); + var baseline = process.WorkingSet64; + var maxDelta = 0L; + var baselineManaged = GC.GetTotalMemory(false); + var maxManagedDelta = 0L; + + for (var i = 1; i <= Iterations; i++) + { + using (var engine = new Engine()) + { + engine.AddPolicy("test.rego", "package test\nx = 1\nmessage = `Hello`"); + _ = engine.EvalRule("data.test.message"); + } + + if (i % LogEvery == 0) + { + process.Refresh(); + var workingSet = process.WorkingSet64; + var managed = GC.GetTotalMemory(false); + var delta = workingSet - baseline; + var managedDelta = managed - baselineManaged; + if (delta > maxDelta) + { + maxDelta = delta; + } + if (managedDelta > maxManagedDelta) + { + maxManagedDelta = managedDelta; + } + Console.WriteLine($"\n\n\u001b[1m{i} ws_mb={workingSet / 1048576.0:F1} managed_mb={managed / 1048576.0:F1} delta_mb={delta / 1048576.0:F1}\u001b[0m\n\n"); + } + } + + if (MaxWorkingSetDeltaBytes is { } limit) + { + Console.WriteLine($"\n\n\u001b[1mSUMMARY: max ws delta {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB); max managed delta {maxManagedDelta / 1048576.0:F1} MB.\u001b[0m\n\n"); + Assert.IsTrue( + maxDelta <= limit, + $"Working set grew by {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB). Managed heap max delta {maxManagedDelta / 1048576.0:F1} MB."); + } + }); + } + + [TestMethod] + public void Engine_create_eval_finalize_does_not_grow_working_set() + { + WithOptionalGlobalRegorusMemoryLimit(() => + { + var process = Process.GetCurrentProcess(); + process.Refresh(); + var baseline = process.WorkingSet64; + var maxDelta = 0L; + var baselineManaged = GC.GetTotalMemory(false); + var maxManagedDelta = 0L; + + for (var i = 1; i <= Iterations; i++) + { + var engine = new Engine(); + engine.AddPolicy("test.rego", "package test\nx = 1\nmessage = `Hello`"); + _ = engine.EvalRule("data.test.message"); + + if (i % GcEvery == 0) + { + ForceFullGc(); + } + + if (i % LogEvery == 0) + { + process.Refresh(); + var workingSet = process.WorkingSet64; + var managed = GC.GetTotalMemory(false); + var delta = workingSet - baseline; + var managedDelta = managed - baselineManaged; + if (delta > maxDelta) + { + maxDelta = delta; + } + if (managedDelta > maxManagedDelta) + { + maxManagedDelta = managedDelta; + } + Console.WriteLine($"\n\n\u001b[1m{i} ws_mb={workingSet / 1048576.0:F1} managed_mb={managed / 1048576.0:F1} delta_mb={delta / 1048576.0:F1}\u001b[0m\n\n"); + } + } + + if (MaxWorkingSetDeltaBytes is { } limit) + { + Console.WriteLine($"\n\n\u001b[1mSUMMARY: max ws delta {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB); max managed delta {maxManagedDelta / 1048576.0:F1} MB.\u001b[0m\n\n"); + Assert.IsTrue( + maxDelta <= limit, + $"Working set grew by {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB). Managed heap max delta {maxManagedDelta / 1048576.0:F1} MB."); + } + }); + } + + + [TestMethod] + public void Rvm_rehydrate_execute_dispose_does_not_grow_working_set() + { + WithOptionalGlobalRegorusMemoryLimit(() => + { + var modules = new[] + { + new PolicyModule("test.rego", "package test\nallow = true"), + }; + + using var compiled = Program.CompileFromModules("{}", modules, new[] { "data.test.allow" }); + var serialized = compiled.SerializeBinary(); + + var process = Process.GetCurrentProcess(); + process.Refresh(); + var baseline = process.WorkingSet64; + var maxDelta = 0L; + var baselineManaged = GC.GetTotalMemory(false); + var maxManagedDelta = 0L; + + for (var i = 1; i <= Iterations; i++) + { + using (var vm = new Rvm()) + using (var program = Program.DeserializeBinary(serialized, out _)) + { + vm.LoadProgram(program); + vm.SetDataJson("{}"); + vm.SetInputJson("{}"); + _ = vm.ExecuteEntryPoint(0); + } + + if (i % LogEvery == 0) + { + process.Refresh(); + var workingSet = process.WorkingSet64; + var managed = GC.GetTotalMemory(false); + var delta = workingSet - baseline; + var managedDelta = managed - baselineManaged; + if (delta > maxDelta) + { + maxDelta = delta; + } + if (managedDelta > maxManagedDelta) + { + maxManagedDelta = managedDelta; + } + Console.WriteLine($"\n\n\u001b[1m{i} ws_mb={workingSet / 1048576.0:F1} managed_mb={managed / 1048576.0:F1} delta_mb={delta / 1048576.0:F1}\u001b[0m\n\n"); + } + } + + if (MaxWorkingSetDeltaBytes is { } limit) + { + Console.WriteLine($"\n\n\u001b[1mSUMMARY: max ws delta {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB); max managed delta {maxManagedDelta / 1048576.0:F1} MB.\u001b[0m\n\n"); + Assert.IsTrue( + maxDelta <= limit, + $"Working set grew by {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB). Managed heap max delta {maxManagedDelta / 1048576.0:F1} MB."); + } + }); + } + + [TestMethod] + public void Rvm_rehydrate_execute_finalize_does_not_grow_working_set() + { + WithOptionalGlobalRegorusMemoryLimit(() => + { + var modules = new[] + { + new PolicyModule("test.rego", "package test\nallow = true"), + }; + + using var compiled = Program.CompileFromModules("{}", modules, new[] { "data.test.allow" }); + var serialized = compiled.SerializeBinary(); + + var process = Process.GetCurrentProcess(); + process.Refresh(); + var baseline = process.WorkingSet64; + var maxDelta = 0L; + var baselineManaged = GC.GetTotalMemory(false); + var maxManagedDelta = 0L; + + for (var i = 1; i <= Iterations; i++) + { + var vm = new Rvm(); + var program = Program.DeserializeBinary(serialized, out _); + vm.LoadProgram(program); + vm.SetDataJson("{}"); + vm.SetInputJson("{}"); + _ = vm.ExecuteEntryPoint(0); + + if (i % GcEvery == 0) + { + ForceFullGc(); + } + + if (i % LogEvery == 0) + { + process.Refresh(); + var workingSet = process.WorkingSet64; + var managed = GC.GetTotalMemory(false); + var delta = workingSet - baseline; + var managedDelta = managed - baselineManaged; + if (delta > maxDelta) + { + maxDelta = delta; + } + if (managedDelta > maxManagedDelta) + { + maxManagedDelta = managedDelta; + } + Console.WriteLine($"\n\n\u001b[1m{i} ws_mb={workingSet / 1048576.0:F1} managed_mb={managed / 1048576.0:F1} delta_mb={delta / 1048576.0:F1}\u001b[0m\n\n"); + } + } + + if (MaxWorkingSetDeltaBytes is { } limit) + { + Console.WriteLine($"\n\n\u001b[1mSUMMARY: max ws delta {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB); max managed delta {maxManagedDelta / 1048576.0:F1} MB.\u001b[0m\n\n"); + Assert.IsTrue( + maxDelta <= limit, + $"Working set grew by {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB). Managed heap max delta {maxManagedDelta / 1048576.0:F1} MB."); + } + }); + } +} diff --git a/bindings/csharp/Regorus.Tests/RegorusTests.cs b/bindings/csharp/Regorus.Tests/RegorusTests.cs index a98eb1b..c4aa7ca 100644 --- a/bindings/csharp/Regorus.Tests/RegorusTests.cs +++ b/bindings/csharp/Regorus.Tests/RegorusTests.cs @@ -193,10 +193,19 @@ public class RegorusTests var result = engine.GetPolicyPackageNames(); - var packageNames = JsonNode.Parse(result!); + Assert.IsNotNull(result); - Assert.AreEqual("test", packageNames![0]["package_name"].ToString()); - Assert.AreEqual("test.nested.name", packageNames![1]["package_name"].ToString()); + var packageNames = JsonNode.Parse(result); + Assert.IsNotNull(packageNames); + + var packageArray = packageNames.AsArray(); + var firstPackage = packageArray[0]?.AsObject(); + var secondPackage = packageArray[1]?.AsObject(); + + Assert.IsNotNull(firstPackage); + Assert.IsNotNull(secondPackage); + Assert.AreEqual("test", firstPackage!["package_name"]!.GetValue()); + Assert.AreEqual("test.nested.name", secondPackage!["package_name"]!.GetValue()); } [TestMethod] @@ -209,71 +218,84 @@ public class RegorusTests var result = engine.GetPolicyParameters(); - var parameters = JsonNode.Parse(result!); + Assert.IsNotNull(result); - Assert.AreEqual(1, parameters![0]["parameters"].AsArray().Count); - Assert.AreEqual(1, parameters![0]["modifiers"].AsArray().Count); + var parameters = JsonNode.Parse(result); + Assert.IsNotNull(parameters); - Assert.AreEqual("a", parameters![0]["parameters"][0]["name"].ToString()); - Assert.AreEqual("b", parameters![0]["modifiers"][0]["name"].ToString()); + var parametersArray = parameters.AsArray(); + var firstEntry = parametersArray[0]?.AsObject(); + Assert.IsNotNull(firstEntry); + + var parameterList = firstEntry!["parameters"]!.AsArray(); + var modifierList = firstEntry["modifiers"]!.AsArray(); + + Assert.AreEqual(1, parameterList.Count); + Assert.AreEqual(1, modifierList.Count); + + var parameterName = parameterList[0]?.AsObject()?["name"]?.GetValue(); + var modifierName = modifierList[0]?.AsObject()?["name"]?.GetValue(); + + Assert.AreEqual("a", parameterName); + Assert.AreEqual("b", modifierName); } - [TestMethod] - public void Global_memory_limit_can_be_set_and_cleared() - { + [TestMethod] + public void Global_memory_limit_can_be_set_and_cleared() + { lock (LimitLock) { - using var guard = new MemoryLimitScope(); + using var guard = new MemoryLimitScope(); - MemoryLimits.SetGlobalMemoryLimit(null); - Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit()); - - const ulong limit = 32 * 1024; - MemoryLimits.SetGlobalMemoryLimit(limit); - Assert.AreEqual(limit, MemoryLimits.GetGlobalMemoryLimit()); - - MemoryLimits.SetGlobalMemoryLimit(null); - Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit()); - } - } - - [TestMethod] - public void Memory_limit_violations_surface_from_engine_calls() - { - lock (LimitLock) - { - using var guard = new MemoryLimitScope(); - using var engine = new Engine(); - - const ulong limit = 1; - var payload = new string('x', 128 * 1024); - - MemoryLimits.FlushThreadMemoryCounters(); - MemoryLimits.SetGlobalMemoryLimit(limit); - - try - { - var ex = Assert.ThrowsException( - () => engine.SetInputJson($"{{\"payload\":\"{payload}\"}}")); - StringAssert.Contains(ex.Message, "execution exceeded memory limit"); - } - finally - { MemoryLimits.SetGlobalMemoryLimit(null); - MemoryLimits.FlushThreadMemoryCounters(); - } - } - } + Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit()); - [TestMethod] - public void Evaluation_fails_when_input_pushes_policy_over_global_limit() - { + const ulong limit = 32 * 1024; + MemoryLimits.SetGlobalMemoryLimit(limit); + Assert.AreEqual(limit, MemoryLimits.GetGlobalMemoryLimit()); + + MemoryLimits.SetGlobalMemoryLimit(null); + Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit()); + } + } + + [TestMethod] + public void Memory_limit_violations_surface_from_engine_calls() + { lock (LimitLock) { - using var guard = new MemoryLimitScope(); - using var engine = new Engine(); + using var guard = new MemoryLimitScope(); + using var engine = new Engine(); - const string policy = """ + const ulong limit = 1; + var payload = new string('x', 128 * 1024); + + MemoryLimits.FlushThreadMemoryCounters(); + MemoryLimits.SetGlobalMemoryLimit(limit); + + try + { + var ex = Assert.ThrowsException( + () => engine.SetInputJson($"{{\"payload\":\"{payload}\"}}")); + StringAssert.Contains(ex.Message, "execution exceeded memory limit"); + } + finally + { + MemoryLimits.SetGlobalMemoryLimit(null); + MemoryLimits.FlushThreadMemoryCounters(); + } + } + } + + [TestMethod] + public void Evaluation_fails_when_input_pushes_policy_over_global_limit() + { + lock (LimitLock) + { + using var guard = new MemoryLimitScope(); + using var engine = new Engine(); + + const string policy = """ package memorylimit import rego.v1 @@ -281,96 +303,152 @@ import rego.v1 stretched := concat("", [input.block | numbers.range(0, input.repeat - 1)[_]]) """; - engine.AddPolicy("memorylimit.rego", policy); + engine.AddPolicy("memorylimit.rego", policy); - MemoryLimits.FlushThreadMemoryCounters(); - const ulong limit = 4 * 1024 * 1024; - MemoryLimits.SetGlobalMemoryLimit(limit); + MemoryLimits.FlushThreadMemoryCounters(); + const ulong limit = 4 * 1024 * 1024; + MemoryLimits.SetGlobalMemoryLimit(limit); - var block = new string('x', 16 * 1024); + var block = new string('x', 16 * 1024); - var smallInput = JsonSerializer.Serialize(new { block, repeat = 16 }); - engine.SetInputJson(smallInput); - var smallResult = engine.EvalRule("data.memorylimit.stretched"); - Assert.IsNotNull(smallResult); - var stretched = JsonSerializer.Deserialize(smallResult); - Assert.IsNotNull(stretched, "Policy should return a string result."); - Assert.AreEqual(block.Length * 16, stretched!.Length, "Policy should expand the payload under the limit."); + var smallInput = JsonSerializer.Serialize(new { block, repeat = 16 }); + engine.SetInputJson(smallInput); + var smallResult = engine.EvalRule("data.memorylimit.stretched"); + Assert.IsNotNull(smallResult); + var stretched = JsonSerializer.Deserialize(smallResult); + Assert.IsNotNull(stretched, "Policy should return a string result."); + Assert.AreEqual(block.Length * 16, stretched!.Length, "Policy should expand the payload under the limit."); - var largeInput = JsonSerializer.Serialize(new { block, repeat = 4096 }); - engine.SetInputJson(largeInput); + var largeInput = JsonSerializer.Serialize(new { block, repeat = 4096 }); + engine.SetInputJson(largeInput); - var ex = Assert.ThrowsException( - () => engine.EvalRule("data.memorylimit.stretched")); - StringAssert.Contains(ex.Message, "execution exceeded memory limit"); + var ex = Assert.ThrowsException( + () => engine.EvalRule("data.memorylimit.stretched")); + StringAssert.Contains(ex.Message, "execution exceeded memory limit"); } - } + } - [TestMethod] - public void Thread_flush_threshold_roundtrips() - { + [TestMethod] + public void Thread_flush_threshold_roundtrips() + { lock (LimitLock) { - var original = MemoryLimits.GetThreadMemoryFlushThreshold(); - try - { - const ulong threshold = 256 * 1024; - MemoryLimits.SetThreadFlushThresholdOverride(threshold); - Assert.AreEqual(threshold, MemoryLimits.GetThreadMemoryFlushThreshold()); - - MemoryLimits.SetThreadFlushThresholdOverride(null); - var restored = MemoryLimits.GetThreadMemoryFlushThreshold(); - Assert.IsTrue(restored.HasValue, "Clearing override should restore allocator default."); - if (original.HasValue) + var original = MemoryLimits.GetThreadMemoryFlushThreshold(); + try { - Assert.AreEqual(original, restored); + const ulong threshold = 256 * 1024; + MemoryLimits.SetThreadFlushThresholdOverride(threshold); + Assert.AreEqual(threshold, MemoryLimits.GetThreadMemoryFlushThreshold()); + + MemoryLimits.SetThreadFlushThresholdOverride(null); + var restored = MemoryLimits.GetThreadMemoryFlushThreshold(); + Assert.IsTrue(restored.HasValue, "Clearing override should restore allocator default."); + if (original.HasValue) + { + Assert.AreEqual(original, restored); + } + } + finally + { + MemoryLimits.SetThreadFlushThresholdOverride(original); } - } - finally - { - MemoryLimits.SetThreadFlushThresholdOverride(original); - } } - } - - [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++) + [TestMethod] + public void SetInputJson_has_negligible_allocations_after_warmup() { - engine.SetInputJson(payload); + 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)." + ); } - var after = GC.GetAllocatedBytesForCurrentThread(); - var allocated = Math.Max(0, after - before); - var bytesPerOp = allocated / (double)iterations; + [TestMethod] + public void Disposed_objects_throw_object_disposed_exception() + { + var engine = new Engine(); + engine.Dispose(); + Assert.ThrowsException(() => engine.EvalRule("data.test.message")); - // 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. + var program = Program.CreateEmpty(); + program.Dispose(); + Assert.ThrowsException(() => program.SerializeBinary()); - // 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)." - ); - } + var rvm = new Rvm(); + rvm.Dispose(); + Assert.ThrowsException(() => rvm.Execute()); + + var modules = new[] { new PolicyModule("test.rego", "package test\nallow = true") }; + var compiled = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.test.allow"); + compiled.Dispose(); + Assert.ThrowsException(() => compiled.EvalWithInput("{}")); + } + + [TestMethod] + public void Registry_helpers_return_empty_after_clear() + { + TargetRegistry.Clear(); + Assert.IsTrue(TargetRegistry.IsEmpty); + Assert.AreEqual(0, TargetRegistry.GetNames().Count); + + SchemaRegistry.ClearResources(); + SchemaRegistry.ClearEffects(); + Assert.IsTrue(SchemaRegistry.IsResourceRegistryEmpty); + Assert.IsTrue(SchemaRegistry.IsEffectRegistryEmpty); + Assert.AreEqual(0, SchemaRegistry.GetResourceNames().Count); + Assert.AreEqual(0, SchemaRegistry.GetEffectNames().Count); + } + + [TestMethod] + public void Utf8_marshalling_handles_large_unicode_payloads() + { + var payload = string.Concat(new string('ß', 2048), "-✓-", new string('漢', 1024)); + + using var engine = new Engine(); + engine.AddPolicy("test.rego", "package test\nmessage = input.msg"); + engine.SetInputJson(JsonSerializer.Serialize(new { msg = payload })); + + var result = engine.EvalRule("data.test.message"); + + Assert.IsNotNull(result); + + // Compare by parsing the JSON string to avoid encoder differences across platforms. + var parsed = JsonSerializer.Deserialize(result); + Assert.IsNotNull(parsed); + + Assert.AreEqual(payload, parsed); + } private sealed class MemoryLimitScope : IDisposable { diff --git a/bindings/csharp/Regorus.Tests/RvmProgramTests.cs b/bindings/csharp/Regorus.Tests/RvmProgramTests.cs index fd059a0..ee65c68 100644 --- a/bindings/csharp/Regorus.Tests/RvmProgramTests.cs +++ b/bindings/csharp/Regorus.Tests/RvmProgramTests.cs @@ -96,9 +96,9 @@ allow if { Assert.AreEqual("true", result, "expected allow=true"); } - [TestMethod] - public void Program_host_await_suspend_and_resume_succeeds() - { + [TestMethod] + public void Program_host_await_suspend_and_resume_succeeds() + { var modules = new[] { new PolicyModule("host_await.rego", HostAwaitPolicy) }; var entryPoints = new[] { "data.demo.allow" }; @@ -115,5 +115,5 @@ allow if { var resumed = vm.Resume("{\"tier\":\"gold\"}"); Assert.AreEqual("true", resumed, "expected allow=true after resume"); - } + } } \ No newline at end of file diff --git a/bindings/csharp/Regorus/CompiledPolicy.cs b/bindings/csharp/Regorus/CompiledPolicy.cs index 3cdae76..ec811ec 100644 --- a/bindings/csharp/Regorus/CompiledPolicy.cs +++ b/bindings/csharp/Regorus/CompiledPolicy.cs @@ -3,7 +3,6 @@ using System; using System.Text.Json; -using System.Threading; using Regorus.Internal; #nullable enable @@ -18,20 +17,15 @@ namespace Regorus /// Each instance represents a unique native policy object. /// /// Thread Safety: This class is thread-safe for all operations. Multiple threads - /// can safely call EvalWithInput() concurrently, and Dispose() will safely wait - /// for all active evaluations to complete before freeing resources. No external - /// synchronization is required. + /// can safely call EvalWithInput() concurrently. Dispose() blocks new calls, waits + /// briefly, and defers the native release to the last in-flight caller if needed. + /// No external synchronization is required. /// - public unsafe sealed class CompiledPolicy : IDisposable + public unsafe sealed class CompiledPolicy : SafeHandleWrapper { - private RegorusCompiledPolicyHandle? _handle; - private readonly ManualResetEventSlim _idleEvent = new(initialState: true); - private int _isDisposed; - private int _activeEvaluations; - internal CompiledPolicy(RegorusCompiledPolicyHandle handle) + : base(handle, nameof(CompiledPolicy)) { - _handle = handle ?? throw new ArgumentNullException(nameof(handle)); } /// @@ -45,36 +39,16 @@ namespace Regorus /// Thrown when the policy has been disposed public string? EvalWithInput(string inputJson) { - // Increment active evaluations count - var active = System.Threading.Interlocked.Increment(ref _activeEvaluations); - if (active == 1) + return Internal.Utf8Marshaller.WithUtf8(inputJson, inputPtr => { - _idleEvent.Reset(); - } - try - { - ThrowIfDisposed(); - - return Internal.Utf8Marshaller.WithUtf8(inputJson, inputPtr => + return UseHandle(policyPtr => { - return UseHandle(policyPtr => + unsafe { - unsafe - { - return CheckAndDropResult(Internal.API.regorus_compiled_policy_eval_with_input((Internal.RegorusCompiledPolicy*)policyPtr, (byte*)inputPtr)); - } - }); + return CheckAndDropResult(Internal.API.regorus_compiled_policy_eval_with_input((Internal.RegorusCompiledPolicy*)policyPtr, (byte*)inputPtr)); + } }); - } - finally - { - // Decrement active evaluations count - var remaining = System.Threading.Interlocked.Decrement(ref _activeEvaluations); - if (remaining == 0) - { - _idleEvent.Set(); - } - } + }); } /// @@ -86,7 +60,6 @@ namespace Regorus /// Thrown when the policy has been disposed public PolicyInfo GetPolicyInfo() { - ThrowIfDisposed(); var jsonResult = UseHandle(policyPtr => { unsafe @@ -94,7 +67,7 @@ namespace Regorus return CheckAndDropResult(Internal.API.regorus_compiled_policy_get_policy_info((Internal.RegorusCompiledPolicy*)policyPtr)); } }); - + if (string.IsNullOrEmpty(jsonResult)) { throw new Exception("Failed to get policy info: empty response"); @@ -106,8 +79,8 @@ namespace Regorus { PropertyNameCaseInsensitive = true }; - - return JsonSerializer.Deserialize(jsonResult!, options) + + return JsonSerializer.Deserialize(jsonResult!, options) ?? throw new Exception("Failed to deserialize policy info"); } catch (JsonException ex) @@ -116,106 +89,9 @@ namespace Regorus } } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) - { - var handle = _handle; - if (handle != null) - { - _idleEvent.Wait(); - - handle.Dispose(); - _handle = null; - } - - _idleEvent.Dispose(); - } - } - - private void ThrowIfDisposed() - { - if (_isDisposed != 0 || _handle is null || _handle.IsClosed) - throw new ObjectDisposedException(nameof(CompiledPolicy)); - } - private string? CheckAndDropResult(Internal.RegorusResult result) { - try - { - if (result.status != Internal.RegorusStatus.Ok) - { - var message = Internal.Utf8Marshaller.FromUtf8(result.error_message); - throw result.status.CreateException(message); - } - - return result.data_type switch - { - Internal.RegorusDataType.String => Internal.Utf8Marshaller.FromUtf8(result.output), - Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(), - Internal.RegorusDataType.Integer => result.int_value.ToString(), - Internal.RegorusDataType.None => null, - _ => Internal.Utf8Marshaller.FromUtf8(result.output) - }; - } - finally - { - 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; - } - - internal 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(); - } - } - } - - internal T UseHandleForInterop(Func func) - { - return UseHandle(func); - } - - private void UseHandle(Action action) - { - UseHandle(handlePtr => - { - action(handlePtr); - return null; - }); + return Internal.ResultHelpers.GetStringResult(result); } } } diff --git a/bindings/csharp/Regorus/Compiler.cs b/bindings/csharp/Regorus/Compiler.cs index 260742c..ea03610 100644 --- a/bindings/csharp/Regorus/Compiler.cs +++ b/bindings/csharp/Regorus/Compiler.cs @@ -12,17 +12,17 @@ namespace Regorus /// /// Represents a policy module with an ID and content. /// - public struct PolicyModule + public readonly struct PolicyModule { /// - /// Gets or sets the unique identifier for this policy module. + /// Gets the unique identifier for this policy module. /// - public string Id { get; set; } + public string Id { get; } /// - /// Gets or sets the Rego policy content. + /// Gets the Rego policy content. /// - public string Content { get; set; } + public string Content { get; } /// /// Initializes a new instance of the PolicyModule struct. @@ -53,50 +53,40 @@ namespace Regorus /// Thrown when compilation fails public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IEnumerable modules, string entryPointRule) { - var modulesArray = modules.ToArray(); - - var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length]; - var pinnedStrings = new List(modulesArray.Length * 2); - - try + if (modules is null) { - for (int i = 0; i < modulesArray.Length; i++) + throw new ArgumentNullException(nameof(modules)); + } + + return CompilePolicyWithEntrypoint(dataJson, modules.ToArray(), entryPointRule); + } + + /// + /// Compiles a policy from data and modules with a specific entry point rule. + /// + public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IReadOnlyList modules, string entryPointRule) + { + if (modules is null) + { + throw new ArgumentNullException(nameof(modules)); + } + + using var pinnedModules = Internal.ModuleMarshalling.PinPolicyModules(modules); + + return Utf8Marshaller.WithUtf8(dataJson, dataPtr => + Utf8Marshaller.WithUtf8(entryPointRule, entryPointPtr => { - 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 + unsafe { - id = idPinned.Pointer, - content = contentPinned.Pointer - }; - } - - return Utf8Marshaller.WithUtf8(dataJson, dataPtr => - Utf8Marshaller.WithUtf8(entryPointRule, entryPointPtr => - { - unsafe + fixed (Internal.RegorusPolicyModule* modulesPtr = pinnedModules.Buffer) { - fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules) - { - var result = Internal.API.regorus_compile_policy_with_entrypoint( - (byte*)dataPtr, modulesPtr, (UIntPtr)modulesArray.Length, (byte*)entryPointPtr); + var result = Internal.API.regorus_compile_policy_with_entrypoint( + (byte*)dataPtr, modulesPtr, (UIntPtr)pinnedModules.Length, (byte*)entryPointPtr); - var policy = GetCompiledPolicyResult(result); - return policy; - } + return GetCompiledPolicyResult(result); } - })); - } - finally - { - foreach (var pinned in pinnedStrings) - { - pinned.Dispose(); - } - } + } + })); } /// @@ -110,49 +100,39 @@ namespace Regorus /// Thrown when compilation fails public static CompiledPolicy CompilePolicyForTarget(string dataJson, IEnumerable modules) { - var modulesArray = modules.ToArray(); - - var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length]; - var pinnedStrings = new List(modulesArray.Length * 2); - - try + if (modules is null) { - for (int i = 0; i < modulesArray.Length; i++) + throw new ArgumentNullException(nameof(modules)); + } + + return CompilePolicyForTarget(dataJson, modules.ToArray()); + } + + /// + /// Compiles a target-aware policy from data and modules. + /// + public static CompiledPolicy CompilePolicyForTarget(string dataJson, IReadOnlyList modules) + { + if (modules is null) + { + throw new ArgumentNullException(nameof(modules)); + } + + using var pinnedModules = Internal.ModuleMarshalling.PinPolicyModules(modules); + + return Utf8Marshaller.WithUtf8(dataJson, dataPtr => + { + unsafe { - 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 + fixed (Internal.RegorusPolicyModule* modulesPtr = pinnedModules.Buffer) { - id = idPinned.Pointer, - content = contentPinned.Pointer - }; - } + var result = Internal.API.regorus_compile_policy_for_target( + (byte*)dataPtr, modulesPtr, (UIntPtr)pinnedModules.Length); - return Utf8Marshaller.WithUtf8(dataJson, dataPtr => - { - 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; - } + return GetCompiledPolicyResult(result); } - }); - } - finally - { - foreach (var pinned in pinnedStrings) - { - pinned.Dispose(); } - } + }); } private static CompiledPolicy GetCompiledPolicyResult(Internal.RegorusResult result) diff --git a/bindings/csharp/Regorus/Engine.cs b/bindings/csharp/Regorus/Engine.cs index fbfec73..613f8dc 100644 --- a/bindings/csharp/Regorus/Engine.cs +++ b/bindings/csharp/Regorus/Engine.cs @@ -16,14 +16,11 @@ 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 : IDisposable + public unsafe sealed class Engine : SafeHandleWrapper { - private RegorusEngineHandle? _handle; - private int _isDisposed; - public Engine() + : base(RegorusEngineHandle.Create(), nameof(Engine)) { - _handle = RegorusEngineHandle.Create(); } public static void SetFallbackExecutionTimerConfig(ExecutionTimerConfig config) @@ -37,42 +34,13 @@ namespace Regorus CheckAndDropResult(Regorus.Internal.API.regorus_clear_fallback_execution_timer_config()); } - public void Dispose() - { - Dispose(disposing: true); - - // This object will be cleaned up by the Dispose method. - // Therefore, call GC.SuppressFinalize to - // take this object off the finalization queue - // and prevent finalization code for this object - // from executing a second time. - GC.SuppressFinalize(this); - } - - // Dispose(bool disposing) executes in two distinct scenarios. - // If disposing equals true, the method has been called directly - // or indirectly by a user's code. Managed and unmanaged resources - // can be disposed. - // If disposing equals false, the method has been called by the - // runtime from inside the finalizer and you should not reference - // other objects. Only unmanaged resources can be disposed. - void Dispose(bool disposing) - { - if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) - { - _handle?.Dispose(); - _handle = null; - } - } - private Engine(RegorusEngineHandle handle) + : base(handle, nameof(Engine)) { - _handle = handle ?? throw new ArgumentNullException(nameof(handle)); } public Engine Clone() { - ThrowIfDisposed(); return UseHandle(enginePtr => { unsafe @@ -91,402 +59,198 @@ namespace Regorus public void SetStrictBuiltinErrors(bool strict) { - ThrowIfDisposed(); UseHandle(enginePtr => { - unsafe - { - CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_strict_builtin_errors((Regorus.Internal.RegorusEngine*)enginePtr, strict)); - } + CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_strict_builtin_errors((Regorus.Internal.RegorusEngine*)enginePtr, strict)); }); } public void SetExecutionTimerConfig(ExecutionTimerConfig config) { - ThrowIfDisposed(); var nativeConfig = config.ToNative(); UseHandle(enginePtr => { - unsafe - { - var localConfig = nativeConfig; - CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr, &localConfig)); - } + var localConfig = nativeConfig; + CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr, &localConfig)); }); } public void ClearExecutionTimerConfig() { - ThrowIfDisposed(); UseHandle(enginePtr => { - unsafe - { - CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr)); - } + CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr)); }); } public string? AddPolicy(string path, string rego) { - ThrowIfDisposed(); return Utf8Marshaller.WithUtf8(path, pathPtr => Utf8Marshaller.WithUtf8(rego, regoPtr => - { - unsafe - { - return UseHandle(enginePtr => - { - unsafe - { - return CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr, (byte*)regoPtr)); - } - }); - } - })); + UseHandle(enginePtr => + CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr, (byte*)regoPtr)) + ))); } public void SetRegoV0(bool enable) { - ThrowIfDisposed(); UseHandle(enginePtr => { - unsafe - { - CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_rego_v0((Regorus.Internal.RegorusEngine*)enginePtr, enable)); - } + CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_rego_v0((Regorus.Internal.RegorusEngine*)enginePtr, enable)); }); } public string? AddPolicyFromFile(string path) { - ThrowIfDisposed(); return Utf8Marshaller.WithUtf8(path, pathPtr => { - unsafe - { - return UseHandle(enginePtr => - { - unsafe - { - return CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy_from_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr)); - } - }); - } + return UseHandle(enginePtr => + CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy_from_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr)) + ); }); } public void AddDataJson(string data) { - ThrowIfDisposed(); Utf8Marshaller.WithUtf8(data, dataPtr => { - unsafe + UseHandle(enginePtr => { - UseHandle(enginePtr => - { - unsafe - { - CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)dataPtr)); - } - }); - } + CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)dataPtr)); + }); }); } public void AddDataFromJsonFile(string path) { - ThrowIfDisposed(); Utf8Marshaller.WithUtf8(path, pathPtr => { - unsafe + UseHandle(enginePtr => { - UseHandle(enginePtr => - { - unsafe - { - CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr)); - } - }); - } + CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr)); + }); }); } public void SetInputJson(string input) { - ThrowIfDisposed(); Utf8Marshaller.WithUtf8(input, inputPtr => { - unsafe + UseHandle(enginePtr => { - UseHandle(enginePtr => - { - unsafe - { - CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)inputPtr)); - } - }); - } + CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)inputPtr)); + }); }); } public void SetInputFromJsonFile(string path) { - ThrowIfDisposed(); Utf8Marshaller.WithUtf8(path, pathPtr => { - unsafe + UseHandle(enginePtr => { - UseHandle(enginePtr => - { - unsafe - { - CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr)); - } - }); - } + CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr)); + }); }); } public string? EvalQuery(string query) { - ThrowIfDisposed(); return Utf8Marshaller.WithUtf8(query, queryPtr => { - unsafe - { - return UseHandle(enginePtr => - { - unsafe - { - return CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_query((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)queryPtr)); - } - }); - } + return UseHandle(enginePtr => + CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_query((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)queryPtr)) + ); }); } public string? EvalRule(string rule) { - ThrowIfDisposed(); return Utf8Marshaller.WithUtf8(rule, rulePtr => { - unsafe - { - return UseHandle(enginePtr => - { - unsafe - { - return CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_rule((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)rulePtr)); - } - }); - } + return UseHandle(enginePtr => + CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_rule((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)rulePtr)) + ); }); } public void SetEnableCoverage(bool enable) { - ThrowIfDisposed(); UseHandle(enginePtr => { - unsafe - { - CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_enable_coverage((Regorus.Internal.RegorusEngine*)enginePtr, enable)); - } + CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_enable_coverage((Regorus.Internal.RegorusEngine*)enginePtr, enable)); }); } public void ClearCoverageData() { - ThrowIfDisposed(); UseHandle(enginePtr => { - unsafe - { - CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_coverage_data((Regorus.Internal.RegorusEngine*)enginePtr)); - } + CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_coverage_data((Regorus.Internal.RegorusEngine*)enginePtr)); }); } public string? GetCoverageReport() { - ThrowIfDisposed(); return UseHandle(enginePtr => { - unsafe - { - return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report((Regorus.Internal.RegorusEngine*)enginePtr)); - } + return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report((Regorus.Internal.RegorusEngine*)enginePtr)); }); } public string? GetCoverageReportPretty() { - ThrowIfDisposed(); return UseHandle(enginePtr => { - unsafe - { - return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report_pretty((Regorus.Internal.RegorusEngine*)enginePtr)); - } + return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report_pretty((Regorus.Internal.RegorusEngine*)enginePtr)); }); } public void SetGatherPrints(bool enable) { - ThrowIfDisposed(); UseHandle(enginePtr => { - unsafe - { - CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_gather_prints((Regorus.Internal.RegorusEngine*)enginePtr, enable)); - } + CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_gather_prints((Regorus.Internal.RegorusEngine*)enginePtr, enable)); }); } public string? TakePrints() { - ThrowIfDisposed(); return UseHandle(enginePtr => { - unsafe - { - return CheckAndDropResult(Regorus.Internal.API.regorus_engine_take_prints((Regorus.Internal.RegorusEngine*)enginePtr)); - } + return CheckAndDropResult(Regorus.Internal.API.regorus_engine_take_prints((Regorus.Internal.RegorusEngine*)enginePtr)); }); } public string? GetAstAsJson() { - ThrowIfDisposed(); return UseHandle(enginePtr => { - unsafe - { - return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_ast_as_json((Regorus.Internal.RegorusEngine*)enginePtr)); - } + return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_ast_as_json((Regorus.Internal.RegorusEngine*)enginePtr)); }); } public string? GetPolicyPackageNames() { - ThrowIfDisposed(); return UseHandle(enginePtr => { - unsafe - { - return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_package_names((Regorus.Internal.RegorusEngine*)enginePtr)); - } + return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_package_names((Regorus.Internal.RegorusEngine*)enginePtr)); }); } public string? GetPolicyParameters() { - ThrowIfDisposed(); return UseHandle(enginePtr => { - unsafe - { - return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_parameters((Regorus.Internal.RegorusEngine*)enginePtr)); - } + return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_parameters((Regorus.Internal.RegorusEngine*)enginePtr)); }); } - private static string? StringFromUtf8(IntPtr ptr) + private static string? CheckAndDropResult(Regorus.Internal.RegorusResult result) { - -#if NETSTANDARD2_1 - return Marshal.PtrToStringUTF8(ptr); -#else - int len = 0; - while (Marshal.ReadByte(ptr, len) != 0) { ++len; } - byte[] buffer = new byte[len]; - Marshal.Copy(ptr, buffer, 0, buffer.Length); - return Encoding.UTF8.GetString(buffer); -#endif - } - - private static string? CheckAndDropResult(Regorus.Internal.RegorusResult result) - { - try - { - if (result.status != Regorus.Internal.RegorusStatus.Ok) - { - var message = Utf8Marshaller.FromUtf8(result.error_message); - throw result.status.CreateException(message); - } - - return result.data_type switch - { - Regorus.Internal.RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output), - Regorus.Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(), - Regorus.Internal.RegorusDataType.Integer => result.int_value.ToString(), - Regorus.Internal.RegorusDataType.None => null, - _ => Utf8Marshaller.FromUtf8(result.output) - }; - } - finally - { - Regorus.Internal.API.regorus_result_drop(result); - } - } - - private void ThrowIfDisposed() - { - if (_isDisposed != 0 || _handle is null || _handle.IsClosed) - { - throw new ObjectDisposedException(nameof(Engine)); - } - } - - internal RegorusEngineHandle GetHandleForUse() - { - var handle = _handle; - if (handle is null || handle.IsClosed || handle.IsInvalid) - { - throw new ObjectDisposedException(nameof(Engine)); - } - return handle; - } - - internal void UseHandle(Action action) - { - UseHandle(handlePtr => - { - action(handlePtr); - return null; - }); - } - - internal 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(); - } - } - } - - internal T UseHandleForInterop(Func func) - { - return UseHandle(func); + return ResultHelpers.GetStringResult(result); } } diff --git a/bindings/csharp/Regorus/MemoryLimits.cs b/bindings/csharp/Regorus/MemoryLimits.cs index ef05980..c0d537a 100644 --- a/bindings/csharp/Regorus/MemoryLimits.cs +++ b/bindings/csharp/Regorus/MemoryLimits.cs @@ -89,12 +89,14 @@ namespace Regorus ); } - if (result.int_value < 0) + try { - throw new OverflowException($"{errorContext}: native value was negative ({result.int_value})"); + return checked((ulong)result.int_value); + } + catch (OverflowException ex) + { + throw new OverflowException($"{errorContext}: native value was out of range ({result.int_value})", ex); } - - return (ulong)result.int_value; } finally { diff --git a/bindings/csharp/Regorus/ModuleMarshalling.cs b/bindings/csharp/Regorus/ModuleMarshalling.cs new file mode 100644 index 0000000..2a7983e --- /dev/null +++ b/bindings/csharp/Regorus/ModuleMarshalling.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Buffers; +using System.Collections.Generic; +using Regorus; + +#nullable enable + +namespace Regorus.Internal +{ + internal static unsafe class ModuleMarshalling + { + internal sealed class PinnedPolicyModules : IDisposable + { + private readonly List _pins; + private bool _disposed; + + internal PinnedPolicyModules(RegorusPolicyModule[] buffer, int length, List pins) + { + Buffer = buffer; + Length = length; + _pins = pins; + } + + internal RegorusPolicyModule[] Buffer { get; } + + internal int Length { get; } + + public void Dispose() + { + if (_disposed) + { + return; + } + + foreach (var pin in _pins) + { + pin.Dispose(); + } + + ArrayPool.Shared.Return(Buffer, clearArray: true); + _disposed = true; + } + } + + internal sealed class PinnedEntryPoints : IDisposable + { + private readonly List _pins; + private bool _disposed; + + internal PinnedEntryPoints(IntPtr[] buffer, int length, List pins) + { + Buffer = buffer; + Length = length; + _pins = pins; + } + + internal IntPtr[] Buffer { get; } + + internal int Length { get; } + + public void Dispose() + { + if (_disposed) + { + return; + } + + foreach (var pin in _pins) + { + pin.Dispose(); + } + + ArrayPool.Shared.Return(Buffer, clearArray: true); + _disposed = true; + } + } + + internal static PinnedPolicyModules PinPolicyModules(IReadOnlyList modules) + { + if (modules is null) + { + throw new ArgumentNullException(nameof(modules)); + } + + var count = modules.Count; + var buffer = ArrayPool.Shared.Rent(count); + var pins = new List(count * 2); + + try + { + for (int i = 0; i < count; i++) + { + var idPinned = Utf8Marshaller.Pin(modules[i].Id); + var contentPinned = Utf8Marshaller.Pin(modules[i].Content); + pins.Add(idPinned); + pins.Add(contentPinned); + + buffer[i] = new RegorusPolicyModule + { + id = idPinned.Pointer, + content = contentPinned.Pointer + }; + } + + return new PinnedPolicyModules(buffer, count, pins); + } + catch + { + foreach (var pin in pins) + { + pin.Dispose(); + } + + ArrayPool.Shared.Return(buffer, clearArray: true); + throw; + } + } + + internal static PinnedEntryPoints PinEntryPoints(IReadOnlyList entryPoints) + { + if (entryPoints is null) + { + throw new ArgumentNullException(nameof(entryPoints)); + } + + var count = entryPoints.Count; + var buffer = ArrayPool.Shared.Rent(count); + var pins = new List(count); + + try + { + for (int i = 0; i < count; i++) + { + var entryPinned = Utf8Marshaller.Pin(entryPoints[i]); + pins.Add(entryPinned); + buffer[i] = (IntPtr)entryPinned.Pointer; + } + + return new PinnedEntryPoints(buffer, count, pins); + } + catch + { + foreach (var pin in pins) + { + pin.Dispose(); + } + + ArrayPool.Shared.Return(buffer, clearArray: true); + throw; + } + } + } +} diff --git a/bindings/csharp/Regorus/Program.cs b/bindings/csharp/Regorus/Program.cs index b9d7f0b..27eb79d 100644 --- a/bindings/csharp/Regorus/Program.cs +++ b/bindings/csharp/Regorus/Program.cs @@ -13,14 +13,11 @@ namespace Regorus /// /// Represents a compiled RVM program. /// - public unsafe sealed class Program : IDisposable + public unsafe sealed class Program : SafeHandleWrapper { - private RegorusProgramHandle? _handle; - private int _isDisposed; - private Program(RegorusProgramHandle handle) + : base(handle, nameof(Program)) { - _handle = handle ?? throw new ArgumentNullException(nameof(handle)); } /// @@ -36,63 +33,57 @@ namespace Regorus /// public static Program CompileFromModules(string dataJson, IEnumerable modules, IEnumerable entryPoints) { - var modulesArray = modules.ToArray(); - var entryPointsArray = entryPoints.ToArray(); - if (entryPointsArray.Length == 0) + if (modules is null) + { + throw new ArgumentNullException(nameof(modules)); + } + + if (entryPoints is null) + { + throw new ArgumentNullException(nameof(entryPoints)); + } + + return CompileFromModules(dataJson, modules.ToArray(), entryPoints.ToArray()); + } + + /// + /// Compile an RVM program from modules and entry points. + /// + public static Program CompileFromModules(string dataJson, IReadOnlyList modules, IReadOnlyList entryPoints) + { + if (modules is null) + { + throw new ArgumentNullException(nameof(modules)); + } + + if (entryPoints is null) + { + throw new ArgumentNullException(nameof(entryPoints)); + } + + if (entryPoints.Count == 0) { throw new ArgumentException("At least one entry point is required.", nameof(entryPoints)); } - var nativeModules = new RegorusPolicyModule[modulesArray.Length]; - var pinnedStrings = new List(modulesArray.Length * 2 + entryPointsArray.Length); - var entryPointers = new IntPtr[entryPointsArray.Length]; + using var pinnedModules = ModuleMarshalling.PinPolicyModules(modules); + using var pinnedEntryPoints = ModuleMarshalling.PinEntryPoints(entryPoints); - try + return Utf8Marshaller.WithUtf8(dataJson, dataPtr => { - for (int i = 0; i < modulesArray.Length; i++) + fixed (RegorusPolicyModule* modulesPtr = pinnedModules.Buffer) + fixed (IntPtr* entryPtr = pinnedEntryPoints.Buffer) { - var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id); - var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content); - pinnedStrings.Add(idPinned); - pinnedStrings.Add(contentPinned); + var result = API.regorus_program_compile_from_modules( + (byte*)dataPtr, + modulesPtr, + (UIntPtr)pinnedModules.Length, + (byte**)entryPtr, + (UIntPtr)pinnedEntryPoints.Length); - nativeModules[i] = new RegorusPolicyModule - { - id = idPinned.Pointer, - content = contentPinned.Pointer - }; + return GetProgramResult(result); } - - for (int i = 0; i < entryPointsArray.Length; i++) - { - var entryPinned = Utf8Marshaller.Pin(entryPointsArray[i]); - pinnedStrings.Add(entryPinned); - entryPointers[i] = (IntPtr)entryPinned.Pointer; - } - - return Utf8Marshaller.WithUtf8(dataJson, dataPtr => - { - fixed (RegorusPolicyModule* modulesPtr = nativeModules) - fixed (IntPtr* entryPtr = entryPointers) - { - var result = API.regorus_program_compile_from_modules( - (byte*)dataPtr, - modulesPtr, - (UIntPtr)modulesArray.Length, - (byte**)entryPtr, - (UIntPtr)entryPointsArray.Length); - - return GetProgramResult(result); - } - }); - } - finally - { - foreach (var pinned in pinnedStrings) - { - pinned.Dispose(); - } - } + }); } /// @@ -104,44 +95,48 @@ namespace Regorus { throw new ArgumentNullException(nameof(engine)); } + if (entryPoints is null) + { + throw new ArgumentNullException(nameof(entryPoints)); + } - var entryPointsArray = entryPoints.ToArray(); - if (entryPointsArray.Length == 0) + return CompileFromEngine(engine, entryPoints.ToArray()); + } + + /// + /// Compile an RVM program from an engine instance and entry points. + /// + public static Program CompileFromEngine(Engine engine, IReadOnlyList entryPoints) + { + if (engine is null) + { + throw new ArgumentNullException(nameof(engine)); + } + + if (entryPoints is null) + { + throw new ArgumentNullException(nameof(entryPoints)); + } + + if (entryPoints.Count == 0) { throw new ArgumentException("At least one entry point is required.", nameof(entryPoints)); } - var pinnedStrings = new List(entryPointsArray.Length); - var entryPointers = new IntPtr[entryPointsArray.Length]; - try - { - for (int i = 0; i < entryPointsArray.Length; i++) - { - var entryPinned = Utf8Marshaller.Pin(entryPointsArray[i]); - pinnedStrings.Add(entryPinned); - entryPointers[i] = (IntPtr)entryPinned.Pointer; - } + using var pinnedEntryPoints = ModuleMarshalling.PinEntryPoints(entryPoints); - return engine.UseHandleForInterop(enginePtr => - { - fixed (IntPtr* entryPtr = entryPointers) - { - var result = API.regorus_engine_compile_program_with_entrypoints( - (RegorusEngine*)enginePtr, - (byte**)entryPtr, - (UIntPtr)entryPointsArray.Length); - - return GetProgramResult(result); - } - }); - } - finally + return engine.UseHandleForInterop(enginePtr => { - foreach (var pinned in pinnedStrings) + fixed (IntPtr* entryPtr = pinnedEntryPoints.Buffer) { - pinned.Dispose(); + var result = API.regorus_engine_compile_program_with_entrypoints( + (RegorusEngine*)enginePtr, + (byte**)entryPtr, + (UIntPtr)pinnedEntryPoints.Length); + + return GetProgramResult(result); } - } + }); } /// @@ -169,7 +164,6 @@ namespace Regorus /// public byte[] SerializeBinary() { - ThrowIfDisposed(); return UseHandle(programPtr => { var result = API.regorus_program_serialize_binary((RegorusProgram*)programPtr); @@ -182,70 +176,12 @@ namespace Regorus /// public string? GenerateListing() { - ThrowIfDisposed(); return UseHandle(programPtr => { return CheckAndDropResult(API.regorus_program_generate_listing((RegorusProgram*)programPtr)); }); } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) - { - _handle?.Dispose(); - _handle = null; - } - } - - private void ThrowIfDisposed() - { - if (_isDisposed != 0 || _handle is null || _handle.IsClosed) - { - throw new ObjectDisposedException(nameof(Program)); - } - } - - internal RegorusProgramHandle GetHandleForUse() - { - var handle = _handle; - if (handle is null || handle.IsClosed || handle.IsInvalid) - { - throw new ObjectDisposedException(nameof(Program)); - } - return handle; - } - - internal 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(Program)); - } - - return func(pointer); - } - finally - { - if (addedRef) - { - handle.DangerousRelease(); - } - } - } - private static Program GetProgramResult(RegorusResult result) { try @@ -272,27 +208,7 @@ namespace Regorus private static string? CheckAndDropResult(RegorusResult result) { - try - { - if (result.status != RegorusStatus.Ok) - { - var message = Utf8Marshaller.FromUtf8(result.error_message); - throw result.status.CreateException(message); - } - - return result.data_type switch - { - RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output), - RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(), - RegorusDataType.Integer => result.int_value.ToString(), - RegorusDataType.None => null, - _ => Utf8Marshaller.FromUtf8(result.output) - }; - } - finally - { - API.regorus_result_drop(result); - } + return ResultHelpers.GetStringResult(result); } private static byte[] ExtractBuffer(RegorusResult result) diff --git a/bindings/csharp/Regorus/Regorus.csproj b/bindings/csharp/Regorus/Regorus.csproj index dac5ef6..5385f70 100644 --- a/bindings/csharp/Regorus/Regorus.csproj +++ b/bindings/csharp/Regorus/Regorus.csproj @@ -8,7 +8,7 @@ 10.0 - 0.9.0 + 0.9.1 $(VersionSuffix) README.md MIT AND Apache-2.0 AND BSD-3-Clause diff --git a/bindings/csharp/Regorus/ResultHelpers.cs b/bindings/csharp/Regorus/ResultHelpers.cs new file mode 100644 index 0000000..07c8e33 --- /dev/null +++ b/bindings/csharp/Regorus/ResultHelpers.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +#nullable enable + +namespace Regorus.Internal +{ + internal static unsafe class ResultHelpers + { + internal static string? GetStringResult(RegorusResult result) + { + try + { + if (result.status != RegorusStatus.Ok) + { + var message = Utf8Marshaller.FromUtf8(result.error_message); + throw result.status.CreateException(message); + } + + return result.data_type switch + { + RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output), + RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(), + RegorusDataType.Integer => result.int_value.ToString(), + RegorusDataType.None => null, + _ => Utf8Marshaller.FromUtf8(result.output) + }; + } + finally + { + API.regorus_result_drop(result); + } + } + + internal static bool GetBoolResult(RegorusResult result) + { + try + { + if (result.status != RegorusStatus.Ok) + { + var message = Utf8Marshaller.FromUtf8(result.error_message); + throw result.status.CreateException(message); + } + + return result.data_type == RegorusDataType.Boolean && result.bool_value; + } + finally + { + API.regorus_result_drop(result); + } + } + + internal static long GetIntResult(RegorusResult result) + { + try + { + if (result.status != RegorusStatus.Ok) + { + var message = Utf8Marshaller.FromUtf8(result.error_message); + throw result.status.CreateException(message); + } + + return result.data_type == RegorusDataType.Integer ? result.int_value : 0; + } + finally + { + API.regorus_result_drop(result); + } + } + } +} diff --git a/bindings/csharp/Regorus/Rvm.cs b/bindings/csharp/Regorus/Rvm.cs index b649070..f38ecf2 100644 --- a/bindings/csharp/Regorus/Rvm.cs +++ b/bindings/csharp/Regorus/Rvm.cs @@ -7,22 +7,35 @@ using Regorus.Internal; #nullable enable namespace Regorus { + /// + /// Execution mode for the RVM runtime. + /// + public enum ExecutionMode : byte + { + /// + /// Run to completion without yielding. + /// + RunToCompletion = 0, + + /// + /// Suspendable execution mode. + /// + Suspendable = 1, + } + /// /// Wrapper for the Regorus RVM runtime. /// - public unsafe sealed class Rvm : IDisposable + public unsafe sealed class Rvm : SafeHandleWrapper { - private RegorusRvmHandle? _handle; - private int _isDisposed; - public Rvm() + : base(RegorusRvmHandle.Create(), nameof(Rvm)) { - _handle = RegorusRvmHandle.Create(); } private Rvm(RegorusRvmHandle handle) + : base(handle, nameof(Rvm)) { - _handle = handle ?? throw new ArgumentNullException(nameof(handle)); } /// @@ -47,13 +60,12 @@ namespace Regorus /// public void LoadProgram(Program program) { - ThrowIfDisposed(); if (program is null) { throw new ArgumentNullException(nameof(program)); } - program.UseHandle(programPtr => + program.UseHandleForInterop(programPtr => { UseHandle(vmPtr => { @@ -69,7 +81,6 @@ namespace Regorus /// public void SetDataJson(string dataJson) { - ThrowIfDisposed(); Utf8Marshaller.WithUtf8(dataJson, dataPtr => { UseHandle(vmPtr => @@ -85,7 +96,6 @@ namespace Regorus /// public void SetInputJson(string inputJson) { - ThrowIfDisposed(); Utf8Marshaller.WithUtf8(inputJson, inputPtr => { UseHandle(vmPtr => @@ -101,7 +111,6 @@ namespace Regorus /// public void SetExecutionMode(byte mode) { - ThrowIfDisposed(); UseHandle(vmPtr => { CheckAndDropResult(API.regorus_rvm_set_execution_mode((RegorusRvm*)vmPtr, mode)); @@ -109,12 +118,19 @@ namespace Regorus }); } + /// + /// Set the execution mode. + /// + public void SetExecutionMode(ExecutionMode mode) + { + SetExecutionMode((byte)mode); + } + /// /// Execute the program and return the JSON result. /// public string? Execute() { - ThrowIfDisposed(); return UseHandle(vmPtr => { return CheckAndDropResult(API.regorus_rvm_execute((RegorusRvm*)vmPtr)); @@ -126,7 +142,6 @@ namespace Regorus /// public string? ExecuteEntryPoint(string entryPoint) { - ThrowIfDisposed(); return Utf8Marshaller.WithUtf8(entryPoint, entryPtr => { return UseHandle(vmPtr => @@ -141,7 +156,6 @@ namespace Regorus /// public string? ExecuteEntryPoint(ulong index) { - ThrowIfDisposed(); return UseHandle(vmPtr => { return CheckAndDropResult(API.regorus_rvm_execute_entry_point_by_index((RegorusRvm*)vmPtr, (UIntPtr)index)); @@ -153,7 +167,6 @@ namespace Regorus /// public string? Resume(string? resumeValueJson) { - ThrowIfDisposed(); if (resumeValueJson is null) { return UseHandle(vmPtr => @@ -176,70 +189,12 @@ namespace Regorus /// public string? GetExecutionState() { - ThrowIfDisposed(); return UseHandle(vmPtr => { return CheckAndDropResult(API.regorus_rvm_get_execution_state((RegorusRvm*)vmPtr)); }); } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) - { - _handle?.Dispose(); - _handle = null; - } - } - - private void ThrowIfDisposed() - { - if (_isDisposed != 0 || _handle is null || _handle.IsClosed) - { - throw new ObjectDisposedException(nameof(Rvm)); - } - } - - internal RegorusRvmHandle GetHandleForUse() - { - var handle = _handle; - if (handle is null || handle.IsClosed || handle.IsInvalid) - { - throw new ObjectDisposedException(nameof(Rvm)); - } - return handle; - } - - internal 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(Rvm)); - } - - return func(pointer); - } - finally - { - if (addedRef) - { - handle.DangerousRelease(); - } - } - } - private static Rvm GetRvmResult(RegorusResult result) { try @@ -266,27 +221,7 @@ namespace Regorus private static string? CheckAndDropResult(RegorusResult result) { - try - { - if (result.status != RegorusStatus.Ok) - { - var message = Utf8Marshaller.FromUtf8(result.error_message); - throw result.status.CreateException(message); - } - - return result.data_type switch - { - RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output), - RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(), - RegorusDataType.Integer => result.int_value.ToString(), - RegorusDataType.None => null, - _ => Utf8Marshaller.FromUtf8(result.output) - }; - } - finally - { - API.regorus_result_drop(result); - } + return ResultHelpers.GetStringResult(result); } } } diff --git a/bindings/csharp/Regorus/SafeHandleWrapper.cs b/bindings/csharp/Regorus/SafeHandleWrapper.cs new file mode 100644 index 0000000..491fabb --- /dev/null +++ b/bindings/csharp/Regorus/SafeHandleWrapper.cs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; + +#nullable enable +namespace Regorus +{ + /// + /// Base class for native handle wrappers that coordinates handle usage and disposal. + /// + /// Behavior summary: + /// - UseHandle: blocks Dispose while running; throws ObjectDisposedException if disposal has started or the handle is invalid. + /// - Dispose: marks disposing and blocks new calls; waits briefly for in-flight calls to finish, then defers native release to the last exiting call if needed. + /// - Handles are never exposed directly; derived classes can only work through UseHandle helpers. + /// + /// Concurrency model: + /// - _state tracks lifecycle transitions (Active -> DisposeRequested -> Released). + /// - HandleGate tracks in-flight operations and enforces the "no new calls after Dispose" rule. + /// - SafeHandle is pinned per call via DangerousAddRef to prevent use-after-free while native work runs. + /// - If Dispose times out, the last in-flight caller performs the release to avoid leaks. + /// + public abstract class SafeHandleWrapper : IDisposable + { + private static readonly TimeSpan DefaultDisposeTimeout = TimeSpan.FromMilliseconds(50); + private const int StateActive = 0; + private const int StateDisposeRequested = 1; + private const int StateReleased = 2; + private readonly HandleGate _gate; + private readonly string _ownerName; + private int _state; + private SafeHandle? _handle; + + protected SafeHandleWrapper(SafeHandle handle, string ownerName) + { + // Cache ownership info and initialize the gate before any use to avoid racing disposal. + _handle = handle ?? throw new ArgumentNullException(nameof(handle)); + _ownerName = ownerName ?? throw new ArgumentNullException(nameof(ownerName)); + _gate = new HandleGate(ownerName); + // Default to a very short wait when in-flight calls exist; release is deferred to the last caller if needed. + } + + protected void UseHandle(Action action) + { + // Reuse the generic path to keep add/ref/release in one place. + UseHandle(ptr => + { + action(ptr); + return null; + }); + } + + protected T UseHandle(Func func) + { + // Fast reject if dispose was requested. + if (System.Threading.Volatile.Read(ref _state) != StateActive) + { + throw new ObjectDisposedException(_ownerName); + } + + // Enter gate so Dispose waits for in-flight native calls. + _gate.Enter(); + bool addedRef = false; + SafeHandle? handle = null; + try + { + // Race: Dispose could begin after Enter; GetHandleForUse validates the handle again. + handle = GetHandleForUse(); + // DangerousAddRef pins the SafeHandle so Dispose cannot close it mid-call. + handle.DangerousAddRef(ref addedRef); + var pointer = handle.DangerousGetHandle(); + // Validate pointer after AddRef in case handle became invalid between checks. + if (pointer == IntPtr.Zero) + { + throw new ObjectDisposedException(_ownerName); + } + + return func(pointer); + } + finally + { + // Always release the DangerousAddRef to avoid leaking the native handle. + if (addedRef) + { + handle?.DangerousRelease(); + } + + // Leave gate so Dispose can proceed when the last caller exits. + var idle = _gate.Exit(); + // Race: Dispose may have timed out while we were in-flight. + // The last exiting caller performs the native release to avoid leaks. + if (idle && System.Threading.Volatile.Read(ref _state) == StateDisposeRequested) + { + TryReleaseHandle(); + } + } + } + + internal T UseHandleForInterop(Func func) + { + // Explicit alias for interop-specific call sites. + return UseHandle(func); + } + + internal void UseHandleForInterop(Action action) + { + // Explicit alias for interop-specific call sites. + UseHandle(action); + } + + private void ThrowIfDisposed() + { + // Fast check for dispose state so callers fail deterministically. + if (System.Threading.Volatile.Read(ref _state) != StateActive) + { + throw new ObjectDisposedException(_ownerName); + } + + // Validate the underlying SafeHandle is still usable; avoids races with release. + var handle = _handle; + if (handle is null || handle.IsClosed || handle.IsInvalid) + { + throw new ObjectDisposedException(_ownerName); + } + } + + private SafeHandle GetHandleForUse() + { + // Centralized gate for derived classes to grab the handle safely. + // This is a second line of defense in case disposal began after the initial state check. + var handle = _handle; + if (handle is null || handle.IsClosed || handle.IsInvalid) + { + throw new ObjectDisposedException(_ownerName); + } + return handle; + } + + public void Dispose() + { + // Only the first caller runs disposal; others become no-ops. + if (System.Threading.Interlocked.CompareExchange(ref _state, StateDisposeRequested, StateActive) == StateActive) + { + // Block new calls and wait briefly if there are in-flight operations. + var completed = _gate.TryBeginDispose(DefaultDisposeTimeout, out var hadActive); + if (completed) + { + // Either no active calls or they drained within the short timeout. + TryReleaseHandle(); + } + else + { + // Defer release to the last in-flight caller to avoid leaks without blocking indefinitely. + // Race: if the last in-flight caller already exited, there will be no Exit() to trigger release. + // Re-check active state and release immediately in that case. + if (!hadActive || _gate.IsIdle) + { + TryReleaseHandle(); + } + } + } + + GC.SuppressFinalize(this); + } + + private void TryReleaseHandle() + { + if (System.Threading.Interlocked.CompareExchange(ref _state, StateReleased, StateDisposeRequested) != StateDisposeRequested) + { + return; + } + + // Once released, no caller should be able to observe a valid handle. + // SafeHandle.Dispose closes the native resource; null to prevent reuse after dispose. + _handle?.Dispose(); + _handle = null; + // Release the wait handle resources after disposal completes. + _gate.Dispose(); + } + + /// + /// Tracks in-flight operations and coordinates disposal. + /// + private sealed class HandleGate : IDisposable + { + private readonly string _ownerName; + private readonly System.Threading.ManualResetEventSlim _idle = new(initialState: true); + private int _active; + private int _disposing; + + internal HandleGate(string ownerName) + { + _ownerName = ownerName; + } + + internal void Enter() + { + // If disposal already started, reject new work immediately. + if (System.Threading.Volatile.Read(ref _disposing) != 0) + { + ThrowDisposed(); + } + + // Track active callers; first one resets idle event. + var active = System.Threading.Interlocked.Increment(ref _active); + if (active == 1) + { + _idle.Reset(); + } + + // Re-check disposing to handle races where Dispose began after increment. + if (System.Threading.Volatile.Read(ref _disposing) != 0) + { + Exit(); + ThrowDisposed(); + } + } + + internal bool Exit() + { + // Last caller signals idle so Dispose can continue. + if (System.Threading.Interlocked.Decrement(ref _active) == 0) + { + _idle.Set(); + return true; + } + + return false; + } + + internal bool IsIdle => System.Threading.Volatile.Read(ref _active) == 0; + + internal bool TryBeginDispose(TimeSpan timeout, out bool hadActive) + { + // Set disposing flag once; subsequent calls treat as already disposing. + if (System.Threading.Interlocked.Exchange(ref _disposing, 1) != 0) + { + hadActive = System.Threading.Volatile.Read(ref _active) != 0; + return true; + } + + hadActive = System.Threading.Volatile.Read(ref _active) != 0; + if (!hadActive) + { + // No in-flight callers; disposal can proceed without waiting. + return true; + } + + // Wait for active callers to drain; optional timeout avoids blocking forever. + if (timeout == System.Threading.Timeout.InfiniteTimeSpan) + { + _idle.Wait(); + return true; + } + + // Race note: callers may finish between the timeout decision and Wait call; Wait handles that safely. + return _idle.Wait(timeout); + } + + private void ThrowDisposed() + { + throw new ObjectDisposedException(_ownerName); + } + + public void Dispose() + { + _idle.Dispose(); + } + } + } +} diff --git a/bindings/csharp/Regorus/SafeHandles.cs b/bindings/csharp/Regorus/SafeHandles.cs index cd38f3d..0864536 100644 --- a/bindings/csharp/Regorus/SafeHandles.cs +++ b/bindings/csharp/Regorus/SafeHandles.cs @@ -44,7 +44,7 @@ namespace Regorus protected override bool ReleaseHandle() { - if (!IsInvalid && !IsClosed) + if (!IsInvalid) { unsafe { @@ -76,7 +76,7 @@ namespace Regorus protected override bool ReleaseHandle() { - if (!IsInvalid && !IsClosed) + if (!IsInvalid) { unsafe { @@ -124,7 +124,7 @@ namespace Regorus protected override bool ReleaseHandle() { - if (!IsInvalid && !IsClosed) + if (!IsInvalid) { unsafe { @@ -172,7 +172,7 @@ namespace Regorus protected override bool ReleaseHandle() { - if (!IsInvalid && !IsClosed) + if (!IsInvalid) { unsafe { diff --git a/bindings/csharp/Regorus/SchemaRegistry.cs b/bindings/csharp/Regorus/SchemaRegistry.cs index 9a1f13d..86d3029 100644 --- a/bindings/csharp/Regorus/SchemaRegistry.cs +++ b/bindings/csharp/Regorus/SchemaRegistry.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Text.Json; using Regorus.Internal; #nullable enable @@ -27,7 +29,7 @@ namespace Regorus { unsafe { - CheckAndDropResult(Internal.API.regorus_resource_schema_register((byte*)namePtr, (byte*)schemaPtr)); + ResultHelpers.GetStringResult(Internal.API.regorus_resource_schema_register((byte*)namePtr, (byte*)schemaPtr)); } }); }); @@ -46,7 +48,7 @@ namespace Regorus unsafe { var result = Internal.API.regorus_resource_schema_contains((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -61,7 +63,7 @@ namespace Regorus get { var result = Internal.API.regorus_resource_schema_len(); - return GetIntResult(result); + return ResultHelpers.GetIntResult(result); } } @@ -75,7 +77,7 @@ namespace Regorus get { var result = Internal.API.regorus_resource_schema_is_empty(); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } } @@ -86,7 +88,16 @@ namespace Regorus /// Thrown when the operation fails public static string ListResourceNames() { - return CheckAndDropResult(Internal.API.regorus_resource_schema_list_names()) ?? "[]"; + return ResultHelpers.GetStringResult(Internal.API.regorus_resource_schema_list_names()) ?? "[]"; + } + + /// + /// List all registered resource schema names as managed strings. + /// + public static IReadOnlyList GetResourceNames() + { + var json = ListResourceNames(); + return JsonSerializer.Deserialize(json) ?? Array.Empty(); } /// @@ -102,7 +113,7 @@ namespace Regorus unsafe { var result = Internal.API.regorus_resource_schema_remove((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -113,7 +124,7 @@ namespace Regorus /// Thrown when the operation fails public static void ClearResources() { - CheckAndDropResult(Internal.API.regorus_resource_schema_clear()); + ResultHelpers.GetStringResult(Internal.API.regorus_resource_schema_clear()); } /// @@ -130,7 +141,7 @@ namespace Regorus { unsafe { - CheckAndDropResult(Internal.API.regorus_effect_schema_register((byte*)namePtr, (byte*)schemaPtr)); + ResultHelpers.GetStringResult(Internal.API.regorus_effect_schema_register((byte*)namePtr, (byte*)schemaPtr)); } }); }); @@ -149,7 +160,7 @@ namespace Regorus unsafe { var result = Internal.API.regorus_effect_schema_contains((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -164,7 +175,7 @@ namespace Regorus get { var result = Internal.API.regorus_effect_schema_len(); - return GetIntResult(result); + return ResultHelpers.GetIntResult(result); } } @@ -178,7 +189,7 @@ namespace Regorus get { var result = Internal.API.regorus_effect_schema_is_empty(); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } } @@ -189,7 +200,16 @@ namespace Regorus /// Thrown when the operation fails public static string ListEffectNames() { - return CheckAndDropResult(Internal.API.regorus_effect_schema_list_names()) ?? "[]"; + return ResultHelpers.GetStringResult(Internal.API.regorus_effect_schema_list_names()) ?? "[]"; + } + + /// + /// List all registered effect schema names as managed strings. + /// + public static IReadOnlyList GetEffectNames() + { + var json = ListEffectNames(); + return JsonSerializer.Deserialize(json) ?? Array.Empty(); } /// @@ -205,7 +225,7 @@ namespace Regorus unsafe { var result = Internal.API.regorus_effect_schema_remove((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -216,68 +236,7 @@ namespace Regorus /// Thrown when the operation fails public static void ClearEffects() { - CheckAndDropResult(Internal.API.regorus_effect_schema_clear()); - } - - private static string? CheckAndDropResult(Internal.RegorusResult result) - { - try - { - if (result.status != Internal.RegorusStatus.Ok) - { - var message = Utf8Marshaller.FromUtf8(result.error_message); - throw result.status.CreateException(message); - } - - return result.data_type switch - { - Internal.RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output), - Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(), - Internal.RegorusDataType.Integer => result.int_value.ToString(), - Internal.RegorusDataType.None => null, - _ => Utf8Marshaller.FromUtf8(result.output) - }; - } - finally - { - Internal.API.regorus_result_drop(result); - } - } - - private static bool GetBoolResult(Internal.RegorusResult result) - { - try - { - if (result.status != Internal.RegorusStatus.Ok) - { - var message = Utf8Marshaller.FromUtf8(result.error_message); - throw result.status.CreateException(message); - } - - return result.data_type == Internal.RegorusDataType.Boolean ? result.bool_value : false; - } - finally - { - Internal.API.regorus_result_drop(result); - } - } - - private static long GetIntResult(Internal.RegorusResult result) - { - try - { - if (result.status != Internal.RegorusStatus.Ok) - { - var message = Utf8Marshaller.FromUtf8(result.error_message); - throw result.status.CreateException(message); - } - - return result.data_type == Internal.RegorusDataType.Integer ? result.int_value : 0; - } - finally - { - Internal.API.regorus_result_drop(result); - } + ResultHelpers.GetStringResult(Internal.API.regorus_effect_schema_clear()); } } } diff --git a/bindings/csharp/Regorus/TargetRegistry.cs b/bindings/csharp/Regorus/TargetRegistry.cs index 4f8d97b..838926d 100644 --- a/bindings/csharp/Regorus/TargetRegistry.cs +++ b/bindings/csharp/Regorus/TargetRegistry.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Text.Json; using Regorus.Internal; #nullable enable @@ -26,7 +28,7 @@ namespace Regorus { unsafe { - CheckAndDropResult(Internal.API.regorus_register_target_from_json((byte*)targetPtr)); + ResultHelpers.GetStringResult(Internal.API.regorus_register_target_from_json((byte*)targetPtr)); } }); } @@ -44,7 +46,7 @@ namespace Regorus unsafe { var result = Internal.API.regorus_target_registry_contains((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -56,7 +58,16 @@ namespace Regorus /// Thrown when the operation fails public static string ListNames() { - return CheckAndDropResult(Internal.API.regorus_target_registry_list_names()) ?? "[]"; + return ResultHelpers.GetStringResult(Internal.API.regorus_target_registry_list_names()) ?? "[]"; + } + + /// + /// Get a list of all registered target names as managed strings. + /// + public static IReadOnlyList GetNames() + { + var json = ListNames(); + return JsonSerializer.Deserialize(json) ?? Array.Empty(); } /// @@ -72,7 +83,7 @@ namespace Regorus unsafe { var result = Internal.API.regorus_target_registry_remove((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -83,7 +94,7 @@ namespace Regorus /// Thrown when the operation fails public static void Clear() { - CheckAndDropResult(Internal.API.regorus_target_registry_clear()); + ResultHelpers.GetStringResult(Internal.API.regorus_target_registry_clear()); } /// @@ -96,10 +107,9 @@ namespace Regorus get { var result = Internal.API.regorus_target_registry_len(); - return GetIntResult(result); + return ResultHelpers.GetIntResult(result); } } - /// /// Check if the target registry is empty. /// @@ -110,68 +120,7 @@ namespace Regorus get { var result = Internal.API.regorus_target_registry_is_empty(); - return GetBoolResult(result); - } - } - - private static string? CheckAndDropResult(Internal.RegorusResult result) - { - try - { - if (result.status != Internal.RegorusStatus.Ok) - { - var message = Utf8Marshaller.FromUtf8(result.error_message); - throw result.status.CreateException(message); - } - - return result.data_type switch - { - Internal.RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output), - Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(), - Internal.RegorusDataType.Integer => result.int_value.ToString(), - Internal.RegorusDataType.None => null, - _ => Utf8Marshaller.FromUtf8(result.output) - }; - } - finally - { - Internal.API.regorus_result_drop(result); - } - } - - private static bool GetBoolResult(Internal.RegorusResult result) - { - try - { - if (result.status != Internal.RegorusStatus.Ok) - { - var message = Utf8Marshaller.FromUtf8(result.error_message); - throw result.status.CreateException(message); - } - - return result.data_type == Internal.RegorusDataType.Boolean ? result.bool_value : false; - } - finally - { - Internal.API.regorus_result_drop(result); - } - } - - private static long GetIntResult(Internal.RegorusResult result) - { - try - { - if (result.status != Internal.RegorusStatus.Ok) - { - var message = Utf8Marshaller.FromUtf8(result.error_message); - throw result.status.CreateException(message); - } - - return result.data_type == Internal.RegorusDataType.Integer ? result.int_value : 0; - } - finally - { - Internal.API.regorus_result_drop(result); + return ResultHelpers.GetBoolResult(result); } } } diff --git a/bindings/csharp/Regorus/Utf8Marshaller.cs b/bindings/csharp/Regorus/Utf8Marshaller.cs index 61bf436..d903056 100644 --- a/bindings/csharp/Regorus/Utf8Marshaller.cs +++ b/bindings/csharp/Regorus/Utf8Marshaller.cs @@ -17,10 +17,10 @@ namespace Regorus.Internal /// 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; + // 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 diff --git a/bindings/csharp/TargetExampleApp/Program.cs b/bindings/csharp/TargetExampleApp/Program.cs index 945ab02..7d81593 100644 --- a/bindings/csharp/TargetExampleApp/Program.cs +++ b/bindings/csharp/TargetExampleApp/Program.cs @@ -65,7 +65,7 @@ triplet_count := count([1 | private const string EXECUTION_TIMER_QUERY = "data.limits.timer.triplet_count"; private const int EXECUTION_TIMER_VALUE_COUNT = 40; - private const string RVM_POLICY = """ + private const string RVM_POLICY = """ package demo import rego.v1 @@ -78,7 +78,7 @@ allow if { } """; - private const string RVM_DATA = """ + private const string RVM_DATA = """ { "roles": { "alice": ["admin", "reader"] @@ -86,13 +86,13 @@ allow if { } """; - private const string RVM_INPUT = """ + private const string RVM_INPUT = """ { "user": "alice" } """; - private const string HOST_AWAIT_POLICY = """ + private const string HOST_AWAIT_POLICY = """ package demo import rego.v1 @@ -105,7 +105,7 @@ allow if { } """; - private const string HOST_AWAIT_INPUT = """ + private const string HOST_AWAIT_INPUT = """ { "account": { "id": "acct-1", @@ -216,7 +216,7 @@ allow if { var nonCompliantResult = compiledPolicy.EvalWithInput(NON_COMPLIANT_STORAGE_ACCOUNT); Console.WriteLine($"Result: {nonCompliantResult}"); - + // 4. Demonstrate thread-safe concurrent evaluation Console.WriteLine("\n4. Testing concurrent evaluation from multiple threads:"); DemonstrateConcurrentEvaluation(compiledPolicy); @@ -246,42 +246,44 @@ allow if { }; Console.WriteLine($"Starting {testInputs.Length} concurrent evaluations..."); - - var tasks = testInputs.Select(input => - Task.Run(() => { + + var tasks = testInputs.Select(input => + Task.Run(() => + { var (threadName, json) = input; var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - + // Multiple evaluations per thread to stress test var results = new List(); for (int i = 0; i < 1000; i++) { - var result = compiledPolicy.EvalWithInput(json); + var result = compiledPolicy.EvalWithInput(json) + ?? throw new System.InvalidOperationException("Expected EvalWithInput to return a JSON value."); results.Add(result); } - + stopwatch.Stop(); var microseconds = stopwatch.ElapsedTicks * 1000000 / System.Diagnostics.Stopwatch.Frequency; - + // Verify all results are identical (thread safety) var firstResult = results[0]; var allIdentical = results.All(r => r == firstResult); - + Console.WriteLine($"✓ {threadName}: {results.Count} evaluations in {microseconds}μs, " + $"Results consistent: {allIdentical}"); - + return (threadName, results.Count, microseconds, allIdentical); }) ).ToArray(); // Wait for all threads to complete var results = Task.WhenAll(tasks).Result; - + Console.WriteLine("\nConcurrency test results:"); var totalEvaluations = results.Sum(r => r.Item2); var maxTime = results.Max(r => r.Item3); var allConsistent = results.All(r => r.allIdentical); - + Console.WriteLine($"✓ Total evaluations: {totalEvaluations}"); Console.WriteLine($"✓ Max thread time: {maxTime}μs"); Console.WriteLine($"✓ All threads consistent: {allConsistent}"); @@ -292,28 +294,28 @@ allow if { static void DemonstratePolicyInfo(Regorus.CompiledPolicy compiledPolicy) { Console.WriteLine("Getting policy metadata using GetPolicyInfo()..."); - + try { var policyInfo = compiledPolicy.GetPolicyInfo(); - + Console.WriteLine($"✓ Policy Information Retrieved:"); Console.WriteLine($" Target Name: {policyInfo.TargetName ?? "None"}"); Console.WriteLine($" Effect Rule: {policyInfo.EffectRule ?? "None"}"); Console.WriteLine($" Entrypoint Rule: {policyInfo.EntrypointRule}"); - + Console.WriteLine($" Module IDs ({policyInfo.ModuleIds.Count}):"); foreach (var moduleId in policyInfo.ModuleIds) { Console.WriteLine($" - {moduleId}"); } - + Console.WriteLine($" Applicable Resource Types ({policyInfo.ApplicableResourceTypes.Count}):"); foreach (var resourceType in policyInfo.ApplicableResourceTypes) { Console.WriteLine($" - {resourceType}"); } - + if (policyInfo.Parameters != null && policyInfo.Parameters.Count > 0) { Console.WriteLine($" Policy Parameters:"); @@ -333,7 +335,7 @@ allow if { Console.WriteLine($" Description: {param.Description}"); } } - + if (parameterSet.Modifiers.Count > 0) { Console.WriteLine($" Modifiers ({parameterSet.Modifiers.Count}):"); @@ -348,11 +350,11 @@ allow if { { Console.WriteLine(" No parameter information available"); } - + // Demonstrate JSON serialization of policy info Console.WriteLine("\n✓ Policy Info as JSON:"); - var jsonOptions = new JsonSerializerOptions - { + var jsonOptions = new JsonSerializerOptions + { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; diff --git a/bindings/csharp/TestApp/Program.cs b/bindings/csharp/TestApp/Program.cs index b7e8617..c9b638c 100644 --- a/bindings/csharp/TestApp/Program.cs +++ b/bindings/csharp/TestApp/Program.cs @@ -42,7 +42,8 @@ w.Restart(); // Set input and eval rule. engine.SetInputFromJsonFile("../../../tests/aci/input.json"); -var value = engine.EvalRule("data.framework.mount_overlay"); +var value = engine.EvalRule("data.framework.mount_overlay") + ?? throw new System.InvalidOperationException("Expected EvalRule to return a JSON value."); #if NET8_0_OR_GREATER var valueDoc = System.Text.Json.JsonDocument.Parse(value); diff --git a/bindings/ffi/Cargo.lock b/bindings/ffi/Cargo.lock index c8d1446..e3498a8 100644 --- a/bindings/ffi/Cargo.lock +++ b/bindings/ffi/Cargo.lock @@ -950,7 +950,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "regorus" -version = "0.9.0" +version = "0.9.1" dependencies = [ "anyhow", "bincode", @@ -981,7 +981,7 @@ dependencies = [ [[package]] name = "regorus-ffi" -version = "0.9.0" +version = "0.9.1" dependencies = [ "anyhow", "cbindgen", diff --git a/bindings/ffi/Cargo.toml b/bindings/ffi/Cargo.toml index 5c93b5c..6d9764d 100644 --- a/bindings/ffi/Cargo.toml +++ b/bindings/ffi/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "regorus-ffi" -version = "0.9.0" +version = "0.9.1" edition = "2021" license = "MIT AND Apache-2.0 AND BSD-3-Clause" diff --git a/bindings/java/Cargo.lock b/bindings/java/Cargo.lock index 2dc8af7..573e1de 100644 --- a/bindings/java/Cargo.lock +++ b/bindings/java/Cargo.lock @@ -826,7 +826,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "regorus" -version = "0.9.0" +version = "0.9.1" dependencies = [ "anyhow", "bincode", @@ -856,7 +856,7 @@ dependencies = [ [[package]] name = "regorus-java" -version = "0.9.0" +version = "0.9.1" dependencies = [ "anyhow", "jni", diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml index d15e637..dc6d44a 100644 --- a/bindings/java/Cargo.toml +++ b/bindings/java/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "regorus-java" -version = "0.9.0" +version = "0.9.1" edition = "2021" repository = "https://github.com/microsoft/regorus/bindings/java" description = "Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" diff --git a/bindings/java/pom.xml b/bindings/java/pom.xml index a1124e6..644340c 100644 --- a/bindings/java/pom.xml +++ b/bindings/java/pom.xml @@ -9,7 +9,7 @@ com.microsoft.regorus regorus-java - 0.9.0 + 0.9.1 Regorus Java Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust diff --git a/bindings/python/Cargo.lock b/bindings/python/Cargo.lock index cbfa3a8..ccdd996 100644 --- a/bindings/python/Cargo.lock +++ b/bindings/python/Cargo.lock @@ -885,7 +885,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "regorus" -version = "0.9.0" +version = "0.9.1" dependencies = [ "anyhow", "bincode", @@ -929,7 +929,7 @@ dependencies = [ [[package]] name = "regoruspy" -version = "0.9.0" +version = "0.9.1" dependencies = [ "anyhow", "ordered-float", diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index 0b8bb56..1d721e9 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "regoruspy" -version = "0.9.0" +version = "0.9.1" edition = "2021" repository = "https://github.com/microsoft/regorus/bindings/python" description = "Python bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" diff --git a/bindings/ruby/ext/regorusrb/Cargo.toml b/bindings/ruby/ext/regorusrb/Cargo.toml index 5917dbd..f6c372e 100644 --- a/bindings/ruby/ext/regorusrb/Cargo.toml +++ b/bindings/ruby/ext/regorusrb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "regorusrb" -version = "0.9.0" +version = "0.9.1" edition = "2024" description = "Ruby bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" license = "MIT AND Apache-2.0 AND BSD-3-Clause" diff --git a/bindings/ruby/lib/regorus/version.rb b/bindings/ruby/lib/regorus/version.rb index fc59bab..a4db917 100644 --- a/bindings/ruby/lib/regorus/version.rb +++ b/bindings/ruby/lib/regorus/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Regorus - VERSION = "0.9.0" + VERSION = "0.9.1" end diff --git a/bindings/wasm/Cargo.lock b/bindings/wasm/Cargo.lock index 81435bf..1dd82e6 100644 --- a/bindings/wasm/Cargo.lock +++ b/bindings/wasm/Cargo.lock @@ -883,7 +883,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "regorus" -version = "0.9.0" +version = "0.9.1" dependencies = [ "anyhow", "bincode", @@ -912,7 +912,7 @@ dependencies = [ [[package]] name = "regorusjs" -version = "0.9.0" +version = "0.9.1" dependencies = [ "getrandom 0.2.17", "getrandom 0.3.4", diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 7cb797a..7d0f029 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "regorusjs" -version = "0.9.0" +version = "0.9.1" edition = "2021" repository = "https://github.com/microsoft/regorus/bindings/wasm" description = "WASM bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" diff --git a/xtask/src/tasks/bindings/all.rs b/xtask/src/tasks/bindings/all.rs index 1352c4d..3a1010d 100644 --- a/xtask/src/tasks/bindings/all.rs +++ b/xtask/src/tasks/bindings/all.rs @@ -135,6 +135,9 @@ impl TestAllBindingsCommand { enforce_artifacts: false, force_nuget: false, nuget_dir: None, + test_filter: None, + skip_apps: false, + console_logger: false, } .run()?; diff --git a/xtask/src/tasks/bindings/csharp.rs b/xtask/src/tasks/bindings/csharp.rs index e12e758..dfe6f74 100644 --- a/xtask/src/tasks/bindings/csharp.rs +++ b/xtask/src/tasks/bindings/csharp.rs @@ -308,6 +308,18 @@ pub struct TestCsharpCommand { /// Restore and test using Regorus NuGet artefacts located at DIR. Defaults to bindings/csharp/Regorus/bin/. #[arg(long = "nuget-dir", value_name = "DIR")] pub nuget_dir: Option, + + /// Optional dotnet test filter to apply when running Regorus.Tests. + #[arg(long = "test-filter", value_name = "FILTER")] + pub test_filter: Option, + + /// Skip building/running the C# sample apps (TestApp, TargetExampleApp). + #[arg(long = "skip-apps")] + pub skip_apps: bool, + + /// Emit console logger output for dotnet test. + #[arg(long = "console-logger")] + pub console_logger: bool, } impl TestCsharpCommand { @@ -412,6 +424,9 @@ impl TestCsharpCommand { &package_dir, self.clean, &package_cache, + self.test_filter.as_deref(), + self.skip_apps, + self.console_logger, )?; Ok(()) @@ -424,6 +439,9 @@ fn run_regorus_tests( package_dir: &Path, clean: bool, package_cache: &Path, + test_filter: Option<&str>, + skip_apps: bool, + console_logger: bool, ) -> Result<()> { let nuget_source = package_dir .to_str() @@ -457,9 +475,22 @@ fn run_regorus_tests( test.arg(configuration); test.arg("--arch"); test.arg(dotnet_host_arch()); + if console_logger { + test.arg("--logger"); + test.arg("console;verbosity=detailed"); + } + if let Some(filter) = test_filter { + test.arg("--"); + test.arg("--filter"); + test.arg(filter); + } test.env("NUGET_PACKAGES", package_cache); run_command(test, "dotnet test (Regorus.Tests)")?; + if skip_apps { + return Ok(()); + } + let test_app = workspace.join("bindings/csharp/TestApp"); if clean { clean_msbuild_project(&test_app)?;