diff --git a/CHANGELOG.md b/CHANGELOG.md index fd047e09..5df91d66 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 fddf06b2..9eb995cd 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 45f5a804..400250c7 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 e15b49cc..a1e827fd 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 @@ private static readonly (string PolicyFile, string[] InputFiles)[] PolicyInputFi 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 @@ private static readonly (string PolicyFile, string[] InputFiles)[] PolicyInputFi 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 @@ private static List PrepareSharedCompiledPolicies() { 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 @@ public static void RunCompiledPolicyEvaluationBenchmark() 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 @@ public static void RunCompiledPolicyBenchmark(int threads, bool useSharedPolicie 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 @@ public static void RunCompiledPolicyBenchmark(int threads, bool useSharedPolicie { foreach (var policy in compiledPolicies) { - policy.Dispose(); + DisposeCompiledPolicy(policy); } } @@ -173,8 +173,8 @@ public static void RunCompiledPolicyBenchmark(int threads, bool useSharedPolicie } 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 @@ private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary 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 2db8a13b..b53b033c 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 @@ private static readonly (string PolicyFile, string[] InputFiles)[] PolicyInputFi 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 @@ private static readonly (string PolicyFile, string[] InputFiles)[] PolicyInputFi 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 @@ private static List PrepareClonedEngines() { 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 @@ private static List PrepareClonedEngines() { // Ignore warmup errors } - + engines.Add(engine); } - + return engines; } @@ -97,13 +97,13 @@ public static void RunEngineEvaluationBenchmark() 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 @@ public static void RunEngineEvaluationBenchmark(int threads, bool useClonedEngin 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 @@ public static void RunEngineEvaluationBenchmark(int threads, bool useClonedEngin } 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 @@ private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary { barrier.SignalAndWait(); - + int evaluationCount = 0; var localEvaluationTime = TimeSpan.Zero; - + while (!stopExecution) { // Use different policy for each iteration @@ -217,7 +217,7 @@ private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary 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 ef15f1ea..f7bf8127 100644 --- a/bindings/csharp/Benchmarks/Program.cs +++ b/bindings/csharp/Benchmarks/Program.cs @@ -7,7 +7,7 @@ class Program static void Main(string[] args) { Console.WriteLine("=== Regorus C# Benchmarks ===\n"); - + try { Console.WriteLine("Running Engine Evaluation Benchmark..."); @@ -17,9 +17,9 @@ static void Main(string[] args) { 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 @@ static void Main(string[] args) { 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 26741f45..6cb753e8 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 00000000..e45331c1 --- /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 a98eb1b4..c4aa7ca4 100644 --- a/bindings/csharp/Regorus.Tests/RegorusTests.cs +++ b/bindings/csharp/Regorus.Tests/RegorusTests.cs @@ -193,10 +193,19 @@ public void GetPolicyPackageNames_succeeds() 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 void GetPolicyParameters_succeeds() var result = engine.GetPolicyParameters(); - var parameters = JsonNode.Parse(result!); + Assert.IsNotNull(result); + + var parameters = JsonNode.Parse(result); + Assert.IsNotNull(parameters); + + var parametersArray = parameters.AsArray(); + var firstEntry = parametersArray[0]?.AsObject(); + Assert.IsNotNull(firstEntry); - Assert.AreEqual(1, parameters![0]["parameters"].AsArray().Count); - Assert.AreEqual(1, parameters![0]["modifiers"].AsArray().Count); + var parameterList = firstEntry!["parameters"]!.AsArray(); + var modifierList = firstEntry["modifiers"]!.AsArray(); - Assert.AreEqual("a", parameters![0]["parameters"][0]["name"].ToString()); - Assert.AreEqual("b", parameters![0]["modifiers"][0]["name"].ToString()); + 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()); + MemoryLimits.SetGlobalMemoryLimit(null); + Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit()); - const ulong limit = 32 * 1024; - MemoryLimits.SetGlobalMemoryLimit(limit); - Assert.AreEqual(limit, MemoryLimits.GetGlobalMemoryLimit()); + const ulong limit = 32 * 1024; + MemoryLimits.SetGlobalMemoryLimit(limit); + Assert.AreEqual(limit, MemoryLimits.GetGlobalMemoryLimit()); - MemoryLimits.SetGlobalMemoryLimit(null); - Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit()); + MemoryLimits.SetGlobalMemoryLimit(null); + Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit()); } - } + } - [TestMethod] - public void Memory_limit_violations_surface_from_engine_calls() - { + [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); + using var guard = new MemoryLimitScope(); + using var engine = new Engine(); - MemoryLimits.FlushThreadMemoryCounters(); - MemoryLimits.SetGlobalMemoryLimit(limit); + const ulong limit = 1; + var payload = new string('x', 128 * 1024); - try - { - var ex = Assert.ThrowsException( - () => engine.SetInputJson($"{{\"payload\":\"{payload}\"}}")); - StringAssert.Contains(ex.Message, "execution exceeded memory limit"); - } - finally - { - MemoryLimits.SetGlobalMemoryLimit(null); 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() - { + [TestMethod] + public void Evaluation_fails_when_input_pushes_policy_over_global_limit() + { 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 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++) + [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); + } - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); + var after = GC.GetAllocatedBytesForCurrentThread(); + var allocated = Math.Max(0, after - before); + var bytesPerOp = allocated / (double)iterations; - const int iterations = 256; - var before = GC.GetAllocatedBytesForCurrentThread(); + // 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. - for (int i = 0; i < iterations; i++) + // 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)." + ); + } + + [TestMethod] + public void Disposed_objects_throw_object_disposed_exception() { - engine.SetInputJson(payload); + var engine = new Engine(); + engine.Dispose(); + Assert.ThrowsException(() => engine.EvalRule("data.test.message")); + + var program = Program.CreateEmpty(); + program.Dispose(); + Assert.ThrowsException(() => program.SerializeBinary()); + + 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("{}")); } - var after = GC.GetAllocatedBytesForCurrentThread(); - var allocated = Math.Max(0, after - before); - var bytesPerOp = allocated / (double)iterations; + [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); + } - // 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. + [TestMethod] + public void Utf8_marshalling_handles_large_unicode_payloads() + { + var payload = string.Concat(new string('ß', 2048), "-✓-", new string('漢', 1024)); - // 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)." - ); - } + 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 fd059a0f..ee65c680 100644 --- a/bindings/csharp/Regorus.Tests/RvmProgramTests.cs +++ b/bindings/csharp/Regorus.Tests/RvmProgramTests.cs @@ -96,9 +96,9 @@ public void Program_compile_from_engine_succeeds() 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 @@ public void Program_host_await_suspend_and_resume_succeeds() 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 3cdae76f..ec811ec9 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 @@ internal CompiledPolicy(RegorusCompiledPolicyHandle handle) /// 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) - { - _idleEvent.Reset(); - } - try + return Internal.Utf8Marshaller.WithUtf8(inputJson, inputPtr => { - 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 @@ internal CompiledPolicy(RegorusCompiledPolicyHandle handle) /// Thrown when the policy has been disposed public PolicyInfo GetPolicyInfo() { - ThrowIfDisposed(); var jsonResult = UseHandle(policyPtr => { unsafe @@ -94,7 +67,7 @@ public PolicyInfo GetPolicyInfo() 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 @@ public PolicyInfo GetPolicyInfo() { 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 @@ public PolicyInfo GetPolicyInfo() } } - 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 260742ca..ea036104 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 @@ public static unsafe class Compiler /// Thrown when compilation fails public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IEnumerable modules, string entryPointRule) { - var modulesArray = modules.ToArray(); + if (modules is null) + { + throw new ArgumentNullException(nameof(modules)); + } - var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length]; - var pinnedStrings = new List(modulesArray.Length * 2); + return CompilePolicyWithEntrypoint(dataJson, modules.ToArray(), entryPointRule); + } - try + /// + /// 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) { - for (int i = 0; i < modulesArray.Length; i++) - { - var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id); - var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content); - pinnedStrings.Add(idPinned); - pinnedStrings.Add(contentPinned); + throw new ArgumentNullException(nameof(modules)); + } - nativeModules[i] = new Internal.RegorusPolicyModule - { - id = idPinned.Pointer, - content = contentPinned.Pointer - }; - } + using var pinnedModules = Internal.ModuleMarshalling.PinPolicyModules(modules); - return Utf8Marshaller.WithUtf8(dataJson, dataPtr => - Utf8Marshaller.WithUtf8(entryPointRule, entryPointPtr => + return Utf8Marshaller.WithUtf8(dataJson, dataPtr => + Utf8Marshaller.WithUtf8(entryPointRule, entryPointPtr => + { + unsafe { - 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 policy = GetCompiledPolicyResult(result); - return policy; - } + var result = Internal.API.regorus_compile_policy_with_entrypoint( + (byte*)dataPtr, modulesPtr, (UIntPtr)pinnedModules.Length, (byte*)entryPointPtr); + + return GetCompiledPolicyResult(result); } - })); - } - finally - { - foreach (var pinned in pinnedStrings) - { - pinned.Dispose(); - } - } + } + })); } /// @@ -110,49 +100,39 @@ public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IEnume /// Thrown when compilation fails public static CompiledPolicy CompilePolicyForTarget(string dataJson, IEnumerable modules) { - var modulesArray = modules.ToArray(); + if (modules is null) + { + throw new ArgumentNullException(nameof(modules)); + } - var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length]; - var pinnedStrings = new List(modulesArray.Length * 2); + return CompilePolicyForTarget(dataJson, modules.ToArray()); + } - try + /// + /// Compiles a target-aware policy from data and modules. + /// + public static CompiledPolicy CompilePolicyForTarget(string dataJson, IReadOnlyList modules) + { + if (modules is null) { - for (int i = 0; i < modulesArray.Length; i++) - { - var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id); - var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content); - pinnedStrings.Add(idPinned); - pinnedStrings.Add(contentPinned); + throw new ArgumentNullException(nameof(modules)); + } - nativeModules[i] = new Internal.RegorusPolicyModule - { - id = idPinned.Pointer, - content = contentPinned.Pointer - }; - } + using var pinnedModules = Internal.ModuleMarshalling.PinPolicyModules(modules); - return Utf8Marshaller.WithUtf8(dataJson, dataPtr => + return Utf8Marshaller.WithUtf8(dataJson, dataPtr => + { + unsafe { - unsafe + fixed (Internal.RegorusPolicyModule* modulesPtr = pinnedModules.Buffer) { - fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules) - { - var result = Internal.API.regorus_compile_policy_for_target( - (byte*)dataPtr, modulesPtr, (UIntPtr)modulesArray.Length); + var result = Internal.API.regorus_compile_policy_for_target( + (byte*)dataPtr, modulesPtr, (UIntPtr)pinnedModules.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 fbfec735..613f8dc2 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 @@ public static void ClearFallbackExecutionTimerConfig() 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 @@ public Engine Clone() 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) - { - -#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) + private static string? CheckAndDropResult(Regorus.Internal.RegorusResult result) { - return UseHandle(func); + return ResultHelpers.GetStringResult(result); } } diff --git a/bindings/csharp/Regorus/MemoryLimits.cs b/bindings/csharp/Regorus/MemoryLimits.cs index ef059804..c0d537a6 100644 --- a/bindings/csharp/Regorus/MemoryLimits.cs +++ b/bindings/csharp/Regorus/MemoryLimits.cs @@ -89,12 +89,14 @@ public static void SetThreadFlushThresholdOverride(ulong? bytes) ); } - 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 00000000..2a7983e1 --- /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 b9d7f0bd..27eb79dd 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 @@ public static Program CreateEmpty() /// 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 ArgumentException("At least one entry point is required.", nameof(entryPoints)); + throw new ArgumentNullException(nameof(modules)); } - var nativeModules = new RegorusPolicyModule[modulesArray.Length]; - var pinnedStrings = new List(modulesArray.Length * 2 + entryPointsArray.Length); - var entryPointers = new IntPtr[entryPointsArray.Length]; - - try + if (entryPoints is null) { - for (int i = 0; i < modulesArray.Length; i++) - { - var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id); - var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content); - pinnedStrings.Add(idPinned); - pinnedStrings.Add(contentPinned); + throw new ArgumentNullException(nameof(entryPoints)); + } - nativeModules[i] = new RegorusPolicyModule - { - id = idPinned.Pointer, - content = contentPinned.Pointer - }; - } + return CompileFromModules(dataJson, modules.ToArray(), entryPoints.ToArray()); + } - for (int i = 0; i < entryPointsArray.Length; i++) - { - var entryPinned = Utf8Marshaller.Pin(entryPointsArray[i]); - pinnedStrings.Add(entryPinned); - entryPointers[i] = (IntPtr)entryPinned.Pointer; - } + /// + /// 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)); + } - 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); + if (entryPoints is null) + { + throw new ArgumentNullException(nameof(entryPoints)); + } - return GetProgramResult(result); - } - }); + if (entryPoints.Count == 0) + { + throw new ArgumentException("At least one entry point is required.", nameof(entryPoints)); } - finally + + using var pinnedModules = ModuleMarshalling.PinPolicyModules(modules); + using var pinnedEntryPoints = ModuleMarshalling.PinEntryPoints(entryPoints); + + return Utf8Marshaller.WithUtf8(dataJson, dataPtr => { - foreach (var pinned in pinnedStrings) + fixed (RegorusPolicyModule* modulesPtr = pinnedModules.Buffer) + fixed (IntPtr* entryPtr = pinnedEntryPoints.Buffer) { - pinned.Dispose(); + var result = API.regorus_program_compile_from_modules( + (byte*)dataPtr, + modulesPtr, + (UIntPtr)pinnedModules.Length, + (byte**)entryPtr, + (UIntPtr)pinnedEntryPoints.Length); + + return GetProgramResult(result); } - } + }); } /// @@ -104,44 +95,48 @@ public static Program CompileFromEngine(Engine engine, IEnumerable entry { throw new ArgumentNullException(nameof(engine)); } - - var entryPointsArray = entryPoints.ToArray(); - if (entryPointsArray.Length == 0) + if (entryPoints is null) { - throw new ArgumentException("At least one entry point is required.", nameof(entryPoints)); + throw new ArgumentNullException(nameof(entryPoints)); } - var pinnedStrings = new List(entryPointsArray.Length); - var entryPointers = new IntPtr[entryPointsArray.Length]; - try + 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) { - for (int i = 0; i < entryPointsArray.Length; i++) - { - var entryPinned = Utf8Marshaller.Pin(entryPointsArray[i]); - pinnedStrings.Add(entryPinned); - entryPointers[i] = (IntPtr)entryPinned.Pointer; - } + throw new ArgumentNullException(nameof(engine)); + } - return engine.UseHandleForInterop(enginePtr => - { - fixed (IntPtr* entryPtr = entryPointers) - { - var result = API.regorus_engine_compile_program_with_entrypoints( - (RegorusEngine*)enginePtr, - (byte**)entryPtr, - (UIntPtr)entryPointsArray.Length); + if (entryPoints is null) + { + throw new ArgumentNullException(nameof(entryPoints)); + } - return GetProgramResult(result); - } - }); + if (entryPoints.Count == 0) + { + throw new ArgumentException("At least one entry point is required.", nameof(entryPoints)); } - finally + + using var pinnedEntryPoints = ModuleMarshalling.PinEntryPoints(entryPoints); + + 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 @@ public static Program DeserializeBinary(byte[] data, out bool isPartial) /// public byte[] SerializeBinary() { - ThrowIfDisposed(); return UseHandle(programPtr => { var result = API.regorus_program_serialize_binary((RegorusProgram*)programPtr); @@ -182,70 +176,12 @@ public byte[] SerializeBinary() /// 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 @@ private static Program GetProgramResult(RegorusResult result) 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 dac5ef62..5385f70f 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 00000000..07c8e331 --- /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 b649070d..f38ecf21 100644 --- a/bindings/csharp/Regorus/Rvm.cs +++ b/bindings/csharp/Regorus/Rvm.cs @@ -8,21 +8,34 @@ namespace Regorus { /// - /// Wrapper for the Regorus RVM runtime. + /// Execution mode for the RVM runtime. /// - public unsafe sealed class Rvm : IDisposable + public enum ExecutionMode : byte { - private RegorusRvmHandle? _handle; - private int _isDisposed; + /// + /// Run to completion without yielding. + /// + RunToCompletion = 0, + + /// + /// Suspendable execution mode. + /// + Suspendable = 1, + } + /// + /// Wrapper for the Regorus RVM runtime. + /// + public unsafe sealed class Rvm : SafeHandleWrapper + { 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 @@ public static Rvm CreateWithPolicy(CompiledPolicy policy) /// 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 @@ public void LoadProgram(Program program) /// public void SetDataJson(string dataJson) { - ThrowIfDisposed(); Utf8Marshaller.WithUtf8(dataJson, dataPtr => { UseHandle(vmPtr => @@ -85,7 +96,6 @@ public void SetDataJson(string dataJson) /// public void SetInputJson(string inputJson) { - ThrowIfDisposed(); Utf8Marshaller.WithUtf8(inputJson, inputPtr => { UseHandle(vmPtr => @@ -101,7 +111,6 @@ public void SetInputJson(string inputJson) /// public void SetExecutionMode(byte mode) { - ThrowIfDisposed(); UseHandle(vmPtr => { CheckAndDropResult(API.regorus_rvm_set_execution_mode((RegorusRvm*)vmPtr, mode)); @@ -109,12 +118,19 @@ public void SetExecutionMode(byte mode) }); } + /// + /// 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 @@ public void SetExecutionMode(byte mode) /// public string? ExecuteEntryPoint(string entryPoint) { - ThrowIfDisposed(); return Utf8Marshaller.WithUtf8(entryPoint, entryPtr => { return UseHandle(vmPtr => @@ -141,7 +156,6 @@ public void SetExecutionMode(byte mode) /// 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 @@ public void SetExecutionMode(byte mode) /// public string? Resume(string? resumeValueJson) { - ThrowIfDisposed(); if (resumeValueJson is null) { return UseHandle(vmPtr => @@ -176,70 +189,12 @@ public void SetExecutionMode(byte mode) /// 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 @@ private static Rvm GetRvmResult(RegorusResult result) 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 00000000..491fabb9 --- /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 cd38f3d6..08645360 100644 --- a/bindings/csharp/Regorus/SafeHandles.cs +++ b/bindings/csharp/Regorus/SafeHandles.cs @@ -44,7 +44,7 @@ internal static RegorusEngineHandle FromPointer(IntPtr pointer) protected override bool ReleaseHandle() { - if (!IsInvalid && !IsClosed) + if (!IsInvalid) { unsafe { @@ -76,7 +76,7 @@ internal static RegorusCompiledPolicyHandle FromPointer(IntPtr pointer) protected override bool ReleaseHandle() { - if (!IsInvalid && !IsClosed) + if (!IsInvalid) { unsafe { @@ -124,7 +124,7 @@ internal static RegorusProgramHandle FromPointer(IntPtr pointer) protected override bool ReleaseHandle() { - if (!IsInvalid && !IsClosed) + if (!IsInvalid) { unsafe { @@ -172,7 +172,7 @@ internal static RegorusRvmHandle FromPointer(IntPtr pointer) 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 9a1f13d5..86d30294 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 @@ public static void RegisterResource(string name, string schemaJson) { 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 @@ public static bool ContainsResource(string name) unsafe { var result = Internal.API.regorus_resource_schema_contains((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -61,7 +63,7 @@ public static long ResourceCount get { var result = Internal.API.regorus_resource_schema_len(); - return GetIntResult(result); + return ResultHelpers.GetIntResult(result); } } @@ -75,7 +77,7 @@ public static bool IsResourceRegistryEmpty get { var result = Internal.API.regorus_resource_schema_is_empty(); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } } @@ -86,7 +88,16 @@ public static bool IsResourceRegistryEmpty /// 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 @@ public static bool RemoveResource(string name) unsafe { var result = Internal.API.regorus_resource_schema_remove((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -113,7 +124,7 @@ public static bool RemoveResource(string name) /// 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 @@ public static void RegisterEffect(string name, string schemaJson) { 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 @@ public static bool ContainsEffect(string name) unsafe { var result = Internal.API.regorus_effect_schema_contains((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -164,7 +175,7 @@ public static long EffectCount get { var result = Internal.API.regorus_effect_schema_len(); - return GetIntResult(result); + return ResultHelpers.GetIntResult(result); } } @@ -178,7 +189,7 @@ public static bool IsEffectRegistryEmpty get { var result = Internal.API.regorus_effect_schema_is_empty(); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } } @@ -189,7 +200,16 @@ public static bool IsEffectRegistryEmpty /// 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 @@ public static bool RemoveEffect(string name) unsafe { var result = Internal.API.regorus_effect_schema_remove((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -216,68 +236,7 @@ public static bool RemoveEffect(string name) /// 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 4f8d97b0..838926d1 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 @@ public static void RegisterFromJson(string targetJson) { 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 @@ public static bool Contains(string name) unsafe { var result = Internal.API.regorus_target_registry_contains((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -56,7 +58,16 @@ public static bool Contains(string name) /// 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 @@ public static bool Remove(string name) unsafe { var result = Internal.API.regorus_target_registry_remove((byte*)namePtr); - return GetBoolResult(result); + return ResultHelpers.GetBoolResult(result); } }); } @@ -83,7 +94,7 @@ public static bool Remove(string name) /// 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 @@ public static long Count 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 @@ public static bool IsEmpty 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 61bf4368..d903056b 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 945ab02c..7d815935 100644 --- a/bindings/csharp/TargetExampleApp/Program.cs +++ b/bindings/csharp/TargetExampleApp/Program.cs @@ -65,7 +65,7 @@ import rego.v1 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 @@ some role in data.roles[input.user] } """; - private const string RVM_DATA = """ + private const string RVM_DATA = """ { "roles": { "alice": ["admin", "reader"] @@ -86,13 +86,13 @@ some role in data.roles[input.user] } """; - 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 @@ import rego.v1 } """; - private const string HOST_AWAIT_INPUT = """ + private const string HOST_AWAIT_INPUT = """ { "account": { "id": "acct-1", @@ -216,7 +216,7 @@ static void DemonstrateTargetFunctionality() 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 @@ static void DemonstrateConcurrentEvaluation(Regorus.CompiledPolicy compiledPolic }; 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 @@ static void DemonstrateConcurrentEvaluation(Regorus.CompiledPolicy compiledPolic 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 @@ static void DemonstratePolicyInfo(Regorus.CompiledPolicy compiledPolicy) Console.WriteLine($" Description: {param.Description}"); } } - + if (parameterSet.Modifiers.Count > 0) { Console.WriteLine($" Modifiers ({parameterSet.Modifiers.Count}):"); @@ -348,11 +350,11 @@ static void DemonstratePolicyInfo(Regorus.CompiledPolicy compiledPolicy) { 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 b7e86179..c9b638cf 100644 --- a/bindings/csharp/TestApp/Program.cs +++ b/bindings/csharp/TestApp/Program.cs @@ -42,7 +42,8 @@ // 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 c8d1446c..e3498a86 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 5c93b5cc..6d9764d0 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 2dc8af7d..573e1de5 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 d15e6375..dc6d44a7 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 a1124e63..644340c7 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 cbfa3a8c..ccdd996e 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 0b8bb560..1d721e91 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 5917dbdc..f6c372e6 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 fc59bab7..a4db917f 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 81435bf2..1dd82e63 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 7cb797a7..7d0f0298 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 1352c4d6..3a1010dd 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 e12e758f..dfe6f741 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)?;