fix(bindings): add SafeHandleWrapper + memory growth checks; bump 0.9.1 (#571)

- Introduce SafeHandleWrapper with gating, short drain wait, and deferred release on last in-flight exit.
- Wire Engine/Program/Rvm/CompiledPolicy to wrapper (centralized handle use, interop helper).
- Add C# memory growth tests (using/finalizer paths) and extend xtask C# runner options.
- Add pooled marshalling utilities, ResultHelpers, and API cleanups; update versions/changelog.

Fixes #570. Closes #554

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2026-02-09 23:52:05 +05:30
committed by GitHub
parent 455d2aa588
commit 96360fa9d8
39 changed files with 1496 additions and 1151 deletions

View File

@@ -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

2
Cargo.lock generated
View File

@@ -1227,7 +1227,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "regorus"
version = "0.9.0"
version = "0.9.1"
dependencies = [
"anyhow",
"bincode",

View File

@@ -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"

View File

@@ -12,7 +12,7 @@ namespace Benchmarks
public class CompiledPolicyEvaluationBenchmark
{
private static readonly string TestDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
Directory.GetCurrentDirectory(),
"..", "..", "..",
"benches", "evaluation", "test_data"
);
@@ -33,7 +33,7 @@ namespace Benchmarks
private static readonly string[] PolicyNames = new[]
{
"rbac_policy",
"api_access_policy",
"api_access_policy",
"data_sensitivity_policy",
"time_based_policy",
"data_processing_policy",
@@ -46,21 +46,21 @@ namespace Benchmarks
private static List<(string Policy, string[] Inputs)> LoadPoliciesWithInputs()
{
var result = new List<(string Policy, string[] Inputs)>();
foreach (var (policyFile, inputFiles) in PolicyInputFiles)
{
var policyPath = Path.Combine(TestDataPath, "policies", policyFile);
var policy = File.ReadAllText(policyPath);
var inputs = inputFiles.Select(inputFile =>
{
var inputPath = Path.Combine(TestDataPath, "inputs", inputFile);
return File.ReadAllText(inputPath);
}).ToArray();
result.Add((policy, inputs));
}
return result;
}
@@ -68,14 +68,14 @@ namespace Benchmarks
{
var policiesWithInputs = LoadPoliciesWithInputs();
var compiledPolicies = new List<CompiledPolicy>();
foreach (var (policy, _) in policiesWithInputs)
{
var modules = new[] { new PolicyModule { Id = "policy.rego", Content = policy } };
var modules = new[] { new PolicyModule("policy.rego", policy) };
var compiled = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.bench.allow");
compiledPolicies.Add(compiled);
}
return compiledPolicies;
}
@@ -84,13 +84,13 @@ namespace Benchmarks
var cpuCount = Environment.ProcessorCount;
var maxThreads = cpuCount * 2;
var threadCounts = new List<int> { 1, 2 };
// Add even numbers from 4 to maxThreads
for (int i = 4; i <= maxThreads; i += 2)
{
threadCounts.Add(i);
}
Console.WriteLine($"Running compiled policy benchmark with max_threads: {maxThreads}");
Console.WriteLine($"Testing with thread counts: {string.Join(", ", threadCounts)}");
Console.WriteLine();
@@ -120,19 +120,19 @@ namespace Benchmarks
const int durationSeconds = 3;
var policiesWithInputs = LoadPoliciesWithInputs();
List<CompiledPolicy>? compiledPolicies = null;
if (useSharedPolicies)
{
compiledPolicies = PrepareSharedCompiledPolicies();
}
Console.WriteLine($"Warming up with {threads} threads for {warmupSeconds} seconds...");
// Warmup phase
var (_, _, _, _) = RunBenchmarkPhase(threads, warmupSeconds, policiesWithInputs, compiledPolicies, useSharedPolicies, isWarmup: true);
Console.WriteLine($"Running benchmark with {threads} threads for {durationSeconds} seconds...");
// Actual benchmark phase
var (totalEvaluations, evaluationTime, policyCounters, allocatedBytes) = RunBenchmarkPhase(threads, durationSeconds, policiesWithInputs, compiledPolicies, useSharedPolicies, isWarmup: false);
@@ -155,7 +155,7 @@ namespace Benchmarks
{
foreach (var policy in compiledPolicies)
{
policy.Dispose();
DisposeCompiledPolicy(policy);
}
}
@@ -173,8 +173,8 @@ namespace Benchmarks
}
private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary<string, int> policyCounters, long allocatedBytes) RunBenchmarkPhase(
int threads,
int durationSeconds,
int threads,
int durationSeconds,
List<(string Policy, string[] Inputs)> policiesWithInputs,
List<CompiledPolicy>? compiledPolicies,
bool useSharedPolicies,
@@ -208,10 +208,10 @@ namespace Benchmarks
}
barrier.SignalAndWait();
int evaluationCount = 0;
var localEvaluationTime = TimeSpan.Zero;
while (!stopExecution)
{
// Use different policy for each iteration
@@ -226,23 +226,29 @@ namespace Benchmarks
{
// Measure only the evaluation call
var evalStopwatch = Stopwatch.StartNew();
if (useSharedPolicies)
{
var result = compiledPolicies![policyIdx].EvalWithInput(input);
}
else
{
// Compile policy in each iteration
var modules = new[] { new PolicyModule { Id = "policy.rego", Content = policy } };
// Compile policy in each iteration.
var modules = new[] { new PolicyModule("policy.rego", policy) };
var compiled = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.bench.allow");
var result = compiled.EvalWithInput(input);
compiled.Dispose();
try
{
var result = compiled.EvalWithInput(input);
}
finally
{
DisposeCompiledPolicy(compiled);
}
}
evalStopwatch.Stop();
localEvaluationTime += evalStopwatch.Elapsed;
// Track successful evaluations (only during actual benchmark, not warmup)
if (!isWarmup)
{
@@ -256,10 +262,10 @@ namespace Benchmarks
{
// Ignore evaluation errors for benchmarking purposes
}
evaluationCount++;
}
// Store the actual evaluation time for this thread
if (!isWarmup)
{
@@ -284,11 +290,23 @@ namespace Benchmarks
var totalEvaluations = policyCounters.Values.Sum();
var totalEvaluationTime = evaluationTimes.Values.Aggregate(TimeSpan.Zero, (sum, time) => sum + time);
// Use pure evaluation time (consistent with Rust benchmark)
var evaluationTime = totalEvaluationTime == TimeSpan.Zero ? stopwatch.Elapsed : totalEvaluationTime;
return (totalEvaluations, evaluationTime, policyCounters, allocatedBytes);
}
private static void DisposeCompiledPolicy(CompiledPolicy policy)
{
try
{
policy.Dispose();
}
catch (TimeoutException ex)
{
Console.WriteLine($"Warning: {ex.Message}");
}
}
}
}

View File

@@ -12,7 +12,7 @@ namespace Benchmarks
public class EngineEvaluationBenchmark
{
private static readonly string TestDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
Directory.GetCurrentDirectory(),
"..", "..", "..",
"benches", "evaluation", "test_data"
);
@@ -33,7 +33,7 @@ namespace Benchmarks
private static readonly string[] PolicyNames = new[]
{
"rbac_policy",
"api_access_policy",
"api_access_policy",
"data_sensitivity_policy",
"time_based_policy",
"data_processing_policy",
@@ -46,21 +46,21 @@ namespace Benchmarks
private static List<(string Policy, string[] Inputs)> LoadPoliciesWithInputs()
{
var result = new List<(string Policy, string[] Inputs)>();
foreach (var (policyFile, inputFiles) in PolicyInputFiles)
{
var policyPath = Path.Combine(TestDataPath, "policies", policyFile);
var policy = File.ReadAllText(policyPath);
var inputs = inputFiles.Select(inputFile =>
{
var inputPath = Path.Combine(TestDataPath, "inputs", inputFile);
return File.ReadAllText(inputPath);
}).ToArray();
result.Add((policy, inputs));
}
return result;
}
@@ -68,12 +68,12 @@ namespace Benchmarks
{
var policiesWithInputs = LoadPoliciesWithInputs();
var engines = new List<Engine>();
foreach (var (policy, _) in policiesWithInputs)
{
var engine = new Engine();
engine.AddPolicy("policy.rego", policy);
// Warm up the engine to ensure it's fully prepared for evaluation
// This prevents each cloned engine from repeating preparation work
engine.SetInputJson("{}");
@@ -85,10 +85,10 @@ namespace Benchmarks
{
// Ignore warmup errors
}
engines.Add(engine);
}
return engines;
}
@@ -97,13 +97,13 @@ namespace Benchmarks
var cpuCount = Environment.ProcessorCount;
var maxThreads = cpuCount * 2;
var threadCounts = new List<int> { 1, 2 };
// Add even numbers from 4 to maxThreads
for (int i = 4; i <= maxThreads; i += 2)
{
threadCounts.Add(i);
}
Console.WriteLine($"Running engine benchmark with max_threads: {maxThreads}");
Console.WriteLine($"Testing with thread counts: {string.Join(", ", threadCounts)}");
Console.WriteLine();
@@ -132,14 +132,14 @@ namespace Benchmarks
const int warmupSeconds = 3;
const int durationSeconds = 3;
var policiesWithInputs = LoadPoliciesWithInputs();
Console.WriteLine($"Warming up with {threads} threads for {warmupSeconds} seconds...");
// Warmup phase
var (_, _, _) = RunBenchmarkPhase(threads, warmupSeconds, policiesWithInputs, useClonedEngines, isWarmup: true);
Console.WriteLine($"Running benchmark with {threads} threads for {durationSeconds} seconds...");
// Actual benchmark phase
var (totalEvaluations, evaluationTime, policyCounters) = RunBenchmarkPhase(threads, durationSeconds, policiesWithInputs, useClonedEngines, isWarmup: false);
@@ -165,8 +165,8 @@ namespace Benchmarks
}
private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary<string, int> policyCounters) RunBenchmarkPhase(
int threads,
int durationSeconds,
int threads,
int durationSeconds,
List<(string Policy, string[] Inputs)> policiesWithInputs,
bool useClonedEngines,
bool isWarmup)
@@ -199,10 +199,10 @@ namespace Benchmarks
tasks[threadId] = Task.Run(() =>
{
barrier.SignalAndWait();
int evaluationCount = 0;
var localEvaluationTime = TimeSpan.Zero;
while (!stopExecution)
{
// Use different policy for each iteration
@@ -217,7 +217,7 @@ namespace Benchmarks
{
// Measure only the engine operations
var evalStopwatch = Stopwatch.StartNew();
Engine engine;
if (useClonedEngines)
{
@@ -228,14 +228,14 @@ namespace Benchmarks
engine = new Engine();
engine.AddPolicy("policy.rego", policy);
}
engine.SetInputJson(input);
var result = engine.EvalRule("data.bench.allow");
engine.Dispose();
evalStopwatch.Stop();
localEvaluationTime += evalStopwatch.Elapsed;
// Track successful evaluations (only during actual benchmark, not warmup)
if (!isWarmup)
{
@@ -249,10 +249,10 @@ namespace Benchmarks
{
// Ignore evaluation errors for benchmarking purposes
}
evaluationCount++;
}
// Store the actual evaluation time for this thread
if (!isWarmup)
{
@@ -283,10 +283,10 @@ namespace Benchmarks
var totalEvaluations = policyCounters.Values.Sum();
var totalEvaluationTime = evaluationTimes.Values.Aggregate(TimeSpan.Zero, (sum, time) => sum + time);
// Use pure evaluation time (consistent with Rust benchmark)
var evaluationTime = totalEvaluationTime == TimeSpan.Zero ? stopwatch.Elapsed : totalEvaluationTime;
return (totalEvaluations, evaluationTime, policyCounters);
}
}

View File

@@ -7,7 +7,7 @@ namespace Benchmarks
static void Main(string[] args)
{
Console.WriteLine("=== Regorus C# Benchmarks ===\n");
try
{
Console.WriteLine("Running Engine Evaluation Benchmark...");
@@ -17,9 +17,9 @@ namespace Benchmarks
{
Console.WriteLine($"Engine benchmark failed: {ex.Message}");
}
Console.WriteLine("\n" + new string('=', 80) + "\n");
try
{
Console.WriteLine("Running Compiled Policy Evaluation Benchmark...");
@@ -29,7 +29,7 @@ namespace Benchmarks
{
Console.WriteLine($"Compiled policy benchmark failed: {ex.Message}");
}
Console.WriteLine("\n=== Benchmarks Complete ===");
}
}

View File

@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<RegorusPackageVersion>0.9.0</RegorusPackageVersion>
<RegorusPackageVersion>0.9.1</RegorusPackageVersion>
<RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix>
</PropertyGroup>

View File

@@ -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.");
}
});
}
}

View File

@@ -193,10 +193,19 @@ public class RegorusTests
var result = engine.GetPolicyPackageNames();
var packageNames = JsonNode.Parse(result!);
Assert.IsNotNull(result);
Assert.AreEqual("test", packageNames![0]["package_name"].ToString());
Assert.AreEqual("test.nested.name", packageNames![1]["package_name"].ToString());
var packageNames = JsonNode.Parse(result);
Assert.IsNotNull(packageNames);
var packageArray = packageNames.AsArray();
var firstPackage = packageArray[0]?.AsObject();
var secondPackage = packageArray[1]?.AsObject();
Assert.IsNotNull(firstPackage);
Assert.IsNotNull(secondPackage);
Assert.AreEqual("test", firstPackage!["package_name"]!.GetValue<string>());
Assert.AreEqual("test.nested.name", secondPackage!["package_name"]!.GetValue<string>());
}
[TestMethod]
@@ -209,71 +218,84 @@ public class RegorusTests
var result = engine.GetPolicyParameters();
var parameters = JsonNode.Parse(result!);
Assert.IsNotNull(result);
Assert.AreEqual(1, parameters![0]["parameters"].AsArray().Count);
Assert.AreEqual(1, parameters![0]["modifiers"].AsArray().Count);
var parameters = JsonNode.Parse(result);
Assert.IsNotNull(parameters);
Assert.AreEqual("a", parameters![0]["parameters"][0]["name"].ToString());
Assert.AreEqual("b", parameters![0]["modifiers"][0]["name"].ToString());
var parametersArray = parameters.AsArray();
var firstEntry = parametersArray[0]?.AsObject();
Assert.IsNotNull(firstEntry);
var parameterList = firstEntry!["parameters"]!.AsArray();
var modifierList = firstEntry["modifiers"]!.AsArray();
Assert.AreEqual(1, parameterList.Count);
Assert.AreEqual(1, modifierList.Count);
var parameterName = parameterList[0]?.AsObject()?["name"]?.GetValue<string>();
var modifierName = modifierList[0]?.AsObject()?["name"]?.GetValue<string>();
Assert.AreEqual("a", parameterName);
Assert.AreEqual("b", modifierName);
}
[TestMethod]
public void Global_memory_limit_can_be_set_and_cleared()
{
[TestMethod]
public void Global_memory_limit_can_be_set_and_cleared()
{
lock (LimitLock)
{
using var guard = new MemoryLimitScope();
using var guard = new MemoryLimitScope();
MemoryLimits.SetGlobalMemoryLimit(null);
Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit());
const ulong limit = 32 * 1024;
MemoryLimits.SetGlobalMemoryLimit(limit);
Assert.AreEqual(limit, MemoryLimits.GetGlobalMemoryLimit());
MemoryLimits.SetGlobalMemoryLimit(null);
Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit());
}
}
[TestMethod]
public void Memory_limit_violations_surface_from_engine_calls()
{
lock (LimitLock)
{
using var guard = new MemoryLimitScope();
using var engine = new Engine();
const ulong limit = 1;
var payload = new string('x', 128 * 1024);
MemoryLimits.FlushThreadMemoryCounters();
MemoryLimits.SetGlobalMemoryLimit(limit);
try
{
var ex = Assert.ThrowsException<InvalidOperationException>(
() => engine.SetInputJson($"{{\"payload\":\"{payload}\"}}"));
StringAssert.Contains(ex.Message, "execution exceeded memory limit");
}
finally
{
MemoryLimits.SetGlobalMemoryLimit(null);
MemoryLimits.FlushThreadMemoryCounters();
}
}
}
Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit());
[TestMethod]
public void Evaluation_fails_when_input_pushes_policy_over_global_limit()
{
const ulong limit = 32 * 1024;
MemoryLimits.SetGlobalMemoryLimit(limit);
Assert.AreEqual(limit, MemoryLimits.GetGlobalMemoryLimit());
MemoryLimits.SetGlobalMemoryLimit(null);
Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit());
}
}
[TestMethod]
public void Memory_limit_violations_surface_from_engine_calls()
{
lock (LimitLock)
{
using var guard = new MemoryLimitScope();
using var engine = new Engine();
using var guard = new MemoryLimitScope();
using var engine = new Engine();
const string policy = """
const ulong limit = 1;
var payload = new string('x', 128 * 1024);
MemoryLimits.FlushThreadMemoryCounters();
MemoryLimits.SetGlobalMemoryLimit(limit);
try
{
var ex = Assert.ThrowsException<InvalidOperationException>(
() => engine.SetInputJson($"{{\"payload\":\"{payload}\"}}"));
StringAssert.Contains(ex.Message, "execution exceeded memory limit");
}
finally
{
MemoryLimits.SetGlobalMemoryLimit(null);
MemoryLimits.FlushThreadMemoryCounters();
}
}
}
[TestMethod]
public void Evaluation_fails_when_input_pushes_policy_over_global_limit()
{
lock (LimitLock)
{
using var guard = new MemoryLimitScope();
using var engine = new Engine();
const string policy = """
package memorylimit
import rego.v1
@@ -281,96 +303,152 @@ import rego.v1
stretched := concat("", [input.block | numbers.range(0, input.repeat - 1)[_]])
""";
engine.AddPolicy("memorylimit.rego", policy);
engine.AddPolicy("memorylimit.rego", policy);
MemoryLimits.FlushThreadMemoryCounters();
const ulong limit = 4 * 1024 * 1024;
MemoryLimits.SetGlobalMemoryLimit(limit);
MemoryLimits.FlushThreadMemoryCounters();
const ulong limit = 4 * 1024 * 1024;
MemoryLimits.SetGlobalMemoryLimit(limit);
var block = new string('x', 16 * 1024);
var block = new string('x', 16 * 1024);
var smallInput = JsonSerializer.Serialize(new { block, repeat = 16 });
engine.SetInputJson(smallInput);
var smallResult = engine.EvalRule("data.memorylimit.stretched");
Assert.IsNotNull(smallResult);
var stretched = JsonSerializer.Deserialize<string>(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<string>(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<InvalidOperationException>(
() => engine.EvalRule("data.memorylimit.stretched"));
StringAssert.Contains(ex.Message, "execution exceeded memory limit");
var ex = Assert.ThrowsException<InvalidOperationException>(
() => engine.EvalRule("data.memorylimit.stretched"));
StringAssert.Contains(ex.Message, "execution exceeded memory limit");
}
}
}
[TestMethod]
public void Thread_flush_threshold_roundtrips()
{
[TestMethod]
public void Thread_flush_threshold_roundtrips()
{
lock (LimitLock)
{
var original = MemoryLimits.GetThreadMemoryFlushThreshold();
try
{
const ulong threshold = 256 * 1024;
MemoryLimits.SetThreadFlushThresholdOverride(threshold);
Assert.AreEqual(threshold, MemoryLimits.GetThreadMemoryFlushThreshold());
MemoryLimits.SetThreadFlushThresholdOverride(null);
var restored = MemoryLimits.GetThreadMemoryFlushThreshold();
Assert.IsTrue(restored.HasValue, "Clearing override should restore allocator default.");
if (original.HasValue)
var original = MemoryLimits.GetThreadMemoryFlushThreshold();
try
{
Assert.AreEqual(original, restored);
const ulong threshold = 256 * 1024;
MemoryLimits.SetThreadFlushThresholdOverride(threshold);
Assert.AreEqual(threshold, MemoryLimits.GetThreadMemoryFlushThreshold());
MemoryLimits.SetThreadFlushThresholdOverride(null);
var restored = MemoryLimits.GetThreadMemoryFlushThreshold();
Assert.IsTrue(restored.HasValue, "Clearing override should restore allocator default.");
if (original.HasValue)
{
Assert.AreEqual(original, restored);
}
}
finally
{
MemoryLimits.SetThreadFlushThresholdOverride(original);
}
}
finally
{
MemoryLimits.SetThreadFlushThresholdOverride(original);
}
}
}
[TestMethod]
public void SetInputJson_has_negligible_allocations_after_warmup()
{
using var engine = new Engine();
const string payload = "{}";
// Warm up the engine and JIT to ensure subsequent measurements are representative.
for (int i = 0; i < 16; i++)
{
engine.SetInputJson(payload);
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
const int iterations = 256;
var before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < iterations; i++)
[TestMethod]
public void SetInputJson_has_negligible_allocations_after_warmup()
{
engine.SetInputJson(payload);
using var engine = new Engine();
const string payload = "{}";
// Warm up the engine and JIT to ensure subsequent measurements are representative.
for (int i = 0; i < 16; i++)
{
engine.SetInputJson(payload);
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
const int iterations = 256;
var before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < iterations; i++)
{
engine.SetInputJson(payload);
}
var after = GC.GetAllocatedBytesForCurrentThread();
var allocated = Math.Max(0, after - before);
var bytesPerOp = allocated / (double)iterations;
// Runtime bookkeeping (delegate caches, GC write barriers) differs across platforms, so
// we measure bytes per call rather than absolute totals and allow a small budget.
// CI will flag regressions where marshalling starts allocating per invocation.
// Allow a small budget for delegates and runtime bookkeeping while still flagging regressions.
Assert.IsTrue(
bytesPerOp <= 512,
$"Expected ≤512 B/op after warmup, but observed {bytesPerOp:F2} B/op (total {allocated} bytes)."
);
}
var after = GC.GetAllocatedBytesForCurrentThread();
var allocated = Math.Max(0, after - before);
var bytesPerOp = allocated / (double)iterations;
[TestMethod]
public void Disposed_objects_throw_object_disposed_exception()
{
var engine = new Engine();
engine.Dispose();
Assert.ThrowsException<ObjectDisposedException>(() => engine.EvalRule("data.test.message"));
// Runtime bookkeeping (delegate caches, GC write barriers) differs across platforms, so
// we measure bytes per call rather than absolute totals and allow a small budget.
// CI will flag regressions where marshalling starts allocating per invocation.
var program = Program.CreateEmpty();
program.Dispose();
Assert.ThrowsException<ObjectDisposedException>(() => program.SerializeBinary());
// Allow a small budget for delegates and runtime bookkeeping while still flagging regressions.
Assert.IsTrue(
bytesPerOp <= 512,
$"Expected ≤512 B/op after warmup, but observed {bytesPerOp:F2} B/op (total {allocated} bytes)."
);
}
var rvm = new Rvm();
rvm.Dispose();
Assert.ThrowsException<ObjectDisposedException>(() => 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<ObjectDisposedException>(() => compiled.EvalWithInput("{}"));
}
[TestMethod]
public void Registry_helpers_return_empty_after_clear()
{
TargetRegistry.Clear();
Assert.IsTrue(TargetRegistry.IsEmpty);
Assert.AreEqual(0, TargetRegistry.GetNames().Count);
SchemaRegistry.ClearResources();
SchemaRegistry.ClearEffects();
Assert.IsTrue(SchemaRegistry.IsResourceRegistryEmpty);
Assert.IsTrue(SchemaRegistry.IsEffectRegistryEmpty);
Assert.AreEqual(0, SchemaRegistry.GetResourceNames().Count);
Assert.AreEqual(0, SchemaRegistry.GetEffectNames().Count);
}
[TestMethod]
public void Utf8_marshalling_handles_large_unicode_payloads()
{
var payload = string.Concat(new string('ß', 2048), "-✓-", new string('漢', 1024));
using var engine = new Engine();
engine.AddPolicy("test.rego", "package test\nmessage = input.msg");
engine.SetInputJson(JsonSerializer.Serialize(new { msg = payload }));
var result = engine.EvalRule("data.test.message");
Assert.IsNotNull(result);
// Compare by parsing the JSON string to avoid encoder differences across platforms.
var parsed = JsonSerializer.Deserialize<string>(result);
Assert.IsNotNull(parsed);
Assert.AreEqual(payload, parsed);
}
private sealed class MemoryLimitScope : IDisposable
{

View File

@@ -96,9 +96,9 @@ allow if {
Assert.AreEqual("true", result, "expected allow=true");
}
[TestMethod]
public void Program_host_await_suspend_and_resume_succeeds()
{
[TestMethod]
public void Program_host_await_suspend_and_resume_succeeds()
{
var modules = new[] { new PolicyModule("host_await.rego", HostAwaitPolicy) };
var entryPoints = new[] { "data.demo.allow" };
@@ -115,5 +115,5 @@ allow if {
var resumed = vm.Resume("{\"tier\":\"gold\"}");
Assert.AreEqual("true", resumed, "expected allow=true after resume");
}
}
}

View File

@@ -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.
/// </summary>
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));
}
/// <summary>
@@ -45,36 +39,16 @@ namespace Regorus
/// <exception cref="ObjectDisposedException">Thrown when the policy has been disposed</exception>
public string? EvalWithInput(string inputJson)
{
// Increment active evaluations count
var active = System.Threading.Interlocked.Increment(ref _activeEvaluations);
if (active == 1)
return Internal.Utf8Marshaller.WithUtf8(inputJson, inputPtr =>
{
_idleEvent.Reset();
}
try
{
ThrowIfDisposed();
return Internal.Utf8Marshaller.WithUtf8(inputJson, inputPtr =>
return UseHandle(policyPtr =>
{
return UseHandle(policyPtr =>
unsafe
{
unsafe
{
return CheckAndDropResult(Internal.API.regorus_compiled_policy_eval_with_input((Internal.RegorusCompiledPolicy*)policyPtr, (byte*)inputPtr));
}
});
return CheckAndDropResult(Internal.API.regorus_compiled_policy_eval_with_input((Internal.RegorusCompiledPolicy*)policyPtr, (byte*)inputPtr));
}
});
}
finally
{
// Decrement active evaluations count
var remaining = System.Threading.Interlocked.Decrement(ref _activeEvaluations);
if (remaining == 0)
{
_idleEvent.Set();
}
}
});
}
/// <summary>
@@ -86,7 +60,6 @@ namespace Regorus
/// <exception cref="ObjectDisposedException">Thrown when the policy has been disposed</exception>
public PolicyInfo GetPolicyInfo()
{
ThrowIfDisposed();
var jsonResult = UseHandle(policyPtr =>
{
unsafe
@@ -94,7 +67,7 @@ namespace Regorus
return CheckAndDropResult(Internal.API.regorus_compiled_policy_get_policy_info((Internal.RegorusCompiledPolicy*)policyPtr));
}
});
if (string.IsNullOrEmpty(jsonResult))
{
throw new Exception("Failed to get policy info: empty response");
@@ -106,8 +79,8 @@ namespace Regorus
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<PolicyInfo>(jsonResult!, options)
return JsonSerializer.Deserialize<PolicyInfo>(jsonResult!, options)
?? throw new Exception("Failed to deserialize policy info");
}
catch (JsonException ex)
@@ -116,106 +89,9 @@ namespace Regorus
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{
var handle = _handle;
if (handle != null)
{
_idleEvent.Wait();
handle.Dispose();
_handle = null;
}
_idleEvent.Dispose();
}
}
private void ThrowIfDisposed()
{
if (_isDisposed != 0 || _handle is null || _handle.IsClosed)
throw new ObjectDisposedException(nameof(CompiledPolicy));
}
private string? CheckAndDropResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = Internal.Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
return result.data_type switch
{
Internal.RegorusDataType.String => Internal.Utf8Marshaller.FromUtf8(result.output),
Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(),
Internal.RegorusDataType.Integer => result.int_value.ToString(),
Internal.RegorusDataType.None => null,
_ => Internal.Utf8Marshaller.FromUtf8(result.output)
};
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private RegorusCompiledPolicyHandle GetHandleForUse()
{
var handle = _handle;
if (handle is null || handle.IsClosed || handle.IsInvalid)
{
throw new ObjectDisposedException(nameof(CompiledPolicy));
}
return handle;
}
internal T UseHandle<T>(Func<IntPtr, T> func)
{
var handle = GetHandleForUse();
bool addedRef = false;
try
{
handle.DangerousAddRef(ref addedRef);
var pointer = handle.DangerousGetHandle();
if (pointer == IntPtr.Zero)
{
throw new ObjectDisposedException(nameof(CompiledPolicy));
}
return func(pointer);
}
finally
{
if (addedRef)
{
handle.DangerousRelease();
}
}
}
internal T UseHandleForInterop<T>(Func<IntPtr, T> func)
{
return UseHandle(func);
}
private void UseHandle(Action<IntPtr> action)
{
UseHandle<object?>(handlePtr =>
{
action(handlePtr);
return null;
});
return Internal.ResultHelpers.GetStringResult(result);
}
}
}

View File

@@ -12,17 +12,17 @@ namespace Regorus
/// <summary>
/// Represents a policy module with an ID and content.
/// </summary>
public struct PolicyModule
public readonly struct PolicyModule
{
/// <summary>
/// Gets or sets the unique identifier for this policy module.
/// Gets the unique identifier for this policy module.
/// </summary>
public string Id { get; set; }
public string Id { get; }
/// <summary>
/// Gets or sets the Rego policy content.
/// Gets the Rego policy content.
/// </summary>
public string Content { get; set; }
public string Content { get; }
/// <summary>
/// Initializes a new instance of the PolicyModule struct.
@@ -53,50 +53,40 @@ namespace Regorus
/// <exception cref="Exception">Thrown when compilation fails</exception>
public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IEnumerable<PolicyModule> modules, string entryPointRule)
{
var modulesArray = modules.ToArray();
var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length];
var pinnedStrings = new List<Utf8Marshaller.PinnedUtf8>(modulesArray.Length * 2);
try
if (modules is null)
{
for (int i = 0; i < modulesArray.Length; i++)
throw new ArgumentNullException(nameof(modules));
}
return CompilePolicyWithEntrypoint(dataJson, modules.ToArray(), entryPointRule);
}
/// <summary>
/// Compiles a policy from data and modules with a specific entry point rule.
/// </summary>
public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IReadOnlyList<PolicyModule> modules, string entryPointRule)
{
if (modules is null)
{
throw new ArgumentNullException(nameof(modules));
}
using var pinnedModules = Internal.ModuleMarshalling.PinPolicyModules(modules);
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
Utf8Marshaller.WithUtf8(entryPointRule, entryPointPtr =>
{
var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id);
var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content);
pinnedStrings.Add(idPinned);
pinnedStrings.Add(contentPinned);
nativeModules[i] = new Internal.RegorusPolicyModule
unsafe
{
id = idPinned.Pointer,
content = contentPinned.Pointer
};
}
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
Utf8Marshaller.WithUtf8(entryPointRule, entryPointPtr =>
{
unsafe
fixed (Internal.RegorusPolicyModule* modulesPtr = pinnedModules.Buffer)
{
fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules)
{
var result = Internal.API.regorus_compile_policy_with_entrypoint(
(byte*)dataPtr, modulesPtr, (UIntPtr)modulesArray.Length, (byte*)entryPointPtr);
var result = Internal.API.regorus_compile_policy_with_entrypoint(
(byte*)dataPtr, modulesPtr, (UIntPtr)pinnedModules.Length, (byte*)entryPointPtr);
var policy = GetCompiledPolicyResult(result);
return policy;
}
return GetCompiledPolicyResult(result);
}
}));
}
finally
{
foreach (var pinned in pinnedStrings)
{
pinned.Dispose();
}
}
}
}));
}
/// <summary>
@@ -110,49 +100,39 @@ namespace Regorus
/// <exception cref="Exception">Thrown when compilation fails</exception>
public static CompiledPolicy CompilePolicyForTarget(string dataJson, IEnumerable<PolicyModule> modules)
{
var modulesArray = modules.ToArray();
var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length];
var pinnedStrings = new List<Utf8Marshaller.PinnedUtf8>(modulesArray.Length * 2);
try
if (modules is null)
{
for (int i = 0; i < modulesArray.Length; i++)
throw new ArgumentNullException(nameof(modules));
}
return CompilePolicyForTarget(dataJson, modules.ToArray());
}
/// <summary>
/// Compiles a target-aware policy from data and modules.
/// </summary>
public static CompiledPolicy CompilePolicyForTarget(string dataJson, IReadOnlyList<PolicyModule> modules)
{
if (modules is null)
{
throw new ArgumentNullException(nameof(modules));
}
using var pinnedModules = Internal.ModuleMarshalling.PinPolicyModules(modules);
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
{
unsafe
{
var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id);
var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content);
pinnedStrings.Add(idPinned);
pinnedStrings.Add(contentPinned);
nativeModules[i] = new Internal.RegorusPolicyModule
fixed (Internal.RegorusPolicyModule* modulesPtr = pinnedModules.Buffer)
{
id = idPinned.Pointer,
content = contentPinned.Pointer
};
}
var result = Internal.API.regorus_compile_policy_for_target(
(byte*)dataPtr, modulesPtr, (UIntPtr)pinnedModules.Length);
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
{
unsafe
{
fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules)
{
var result = Internal.API.regorus_compile_policy_for_target(
(byte*)dataPtr, modulesPtr, (UIntPtr)modulesArray.Length);
var policy = GetCompiledPolicyResult(result);
return policy;
}
return GetCompiledPolicyResult(result);
}
});
}
finally
{
foreach (var pinned in pinnedStrings)
{
pinned.Dispose();
}
}
});
}
private static CompiledPolicy GetCompiledPolicyResult(Internal.RegorusResult result)

View File

@@ -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.
/// </summary>
public unsafe sealed class Engine : IDisposable
public unsafe sealed class Engine : SafeHandleWrapper
{
private RegorusEngineHandle? _handle;
private int _isDisposed;
public Engine()
: base(RegorusEngineHandle.Create(), nameof(Engine))
{
_handle = RegorusEngineHandle.Create();
}
public static void SetFallbackExecutionTimerConfig(ExecutionTimerConfig config)
@@ -37,42 +34,13 @@ namespace Regorus
CheckAndDropResult(Regorus.Internal.API.regorus_clear_fallback_execution_timer_config());
}
public void Dispose()
{
Dispose(disposing: true);
// This object will be cleaned up by the Dispose method.
// Therefore, call GC.SuppressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
// Dispose(bool disposing) executes in two distinct scenarios.
// If disposing equals true, the method has been called directly
// or indirectly by a user's code. Managed and unmanaged resources
// can be disposed.
// If disposing equals false, the method has been called by the
// runtime from inside the finalizer and you should not reference
// other objects. Only unmanaged resources can be disposed.
void Dispose(bool disposing)
{
if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{
_handle?.Dispose();
_handle = null;
}
}
private Engine(RegorusEngineHandle handle)
: base(handle, nameof(Engine))
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
public Engine Clone()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
@@ -91,402 +59,198 @@ namespace Regorus
public void SetStrictBuiltinErrors(bool strict)
{
ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_strict_builtin_errors((Regorus.Internal.RegorusEngine*)enginePtr, strict));
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_strict_builtin_errors((Regorus.Internal.RegorusEngine*)enginePtr, strict));
});
}
public void SetExecutionTimerConfig(ExecutionTimerConfig config)
{
ThrowIfDisposed();
var nativeConfig = config.ToNative();
UseHandle(enginePtr =>
{
unsafe
{
var localConfig = nativeConfig;
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr, &localConfig));
}
var localConfig = nativeConfig;
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr, &localConfig));
});
}
public void ClearExecutionTimerConfig()
{
ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr));
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? AddPolicy(string path, string rego)
{
ThrowIfDisposed();
return Utf8Marshaller.WithUtf8(path, pathPtr =>
Utf8Marshaller.WithUtf8(rego, regoPtr =>
{
unsafe
{
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr, (byte*)regoPtr));
}
});
}
}));
UseHandle(enginePtr =>
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr, (byte*)regoPtr))
)));
}
public void SetRegoV0(bool enable)
{
ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_rego_v0((Regorus.Internal.RegorusEngine*)enginePtr, enable));
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_rego_v0((Regorus.Internal.RegorusEngine*)enginePtr, enable));
});
}
public string? AddPolicyFromFile(string path)
{
ThrowIfDisposed();
return Utf8Marshaller.WithUtf8(path, pathPtr =>
{
unsafe
{
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy_from_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
}
});
}
return UseHandle(enginePtr =>
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy_from_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr))
);
});
}
public void AddDataJson(string data)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(data, dataPtr =>
{
unsafe
UseHandle(enginePtr =>
{
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)dataPtr));
}
});
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)dataPtr));
});
});
}
public void AddDataFromJsonFile(string path)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(path, pathPtr =>
{
unsafe
UseHandle(enginePtr =>
{
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
}
});
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
});
});
}
public void SetInputJson(string input)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(input, inputPtr =>
{
unsafe
UseHandle(enginePtr =>
{
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)inputPtr));
}
});
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)inputPtr));
});
});
}
public void SetInputFromJsonFile(string path)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(path, pathPtr =>
{
unsafe
UseHandle(enginePtr =>
{
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
}
});
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
});
});
}
public string? EvalQuery(string query)
{
ThrowIfDisposed();
return Utf8Marshaller.WithUtf8(query, queryPtr =>
{
unsafe
{
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_query((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)queryPtr));
}
});
}
return UseHandle(enginePtr =>
CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_query((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)queryPtr))
);
});
}
public string? EvalRule(string rule)
{
ThrowIfDisposed();
return Utf8Marshaller.WithUtf8(rule, rulePtr =>
{
unsafe
{
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_rule((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)rulePtr));
}
});
}
return UseHandle(enginePtr =>
CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_rule((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)rulePtr))
);
});
}
public void SetEnableCoverage(bool enable)
{
ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_enable_coverage((Regorus.Internal.RegorusEngine*)enginePtr, enable));
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_enable_coverage((Regorus.Internal.RegorusEngine*)enginePtr, enable));
});
}
public void ClearCoverageData()
{
ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_coverage_data((Regorus.Internal.RegorusEngine*)enginePtr));
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_coverage_data((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? GetCoverageReport()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? GetCoverageReportPretty()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report_pretty((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report_pretty((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public void SetGatherPrints(bool enable)
{
ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_gather_prints((Regorus.Internal.RegorusEngine*)enginePtr, enable));
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_gather_prints((Regorus.Internal.RegorusEngine*)enginePtr, enable));
});
}
public string? TakePrints()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_take_prints((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_take_prints((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? GetAstAsJson()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_ast_as_json((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_ast_as_json((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? GetPolicyPackageNames()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_package_names((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_package_names((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? GetPolicyParameters()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_parameters((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_parameters((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
private static string? StringFromUtf8(IntPtr ptr)
private static string? CheckAndDropResult(Regorus.Internal.RegorusResult result)
{
#if NETSTANDARD2_1
return Marshal.PtrToStringUTF8(ptr);
#else
int len = 0;
while (Marshal.ReadByte(ptr, len) != 0) { ++len; }
byte[] buffer = new byte[len];
Marshal.Copy(ptr, buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
#endif
}
private static string? CheckAndDropResult(Regorus.Internal.RegorusResult result)
{
try
{
if (result.status != Regorus.Internal.RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
return result.data_type switch
{
Regorus.Internal.RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output),
Regorus.Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(),
Regorus.Internal.RegorusDataType.Integer => result.int_value.ToString(),
Regorus.Internal.RegorusDataType.None => null,
_ => Utf8Marshaller.FromUtf8(result.output)
};
}
finally
{
Regorus.Internal.API.regorus_result_drop(result);
}
}
private void ThrowIfDisposed()
{
if (_isDisposed != 0 || _handle is null || _handle.IsClosed)
{
throw new ObjectDisposedException(nameof(Engine));
}
}
internal RegorusEngineHandle GetHandleForUse()
{
var handle = _handle;
if (handle is null || handle.IsClosed || handle.IsInvalid)
{
throw new ObjectDisposedException(nameof(Engine));
}
return handle;
}
internal void UseHandle(Action<IntPtr> action)
{
UseHandle<object?>(handlePtr =>
{
action(handlePtr);
return null;
});
}
internal T UseHandle<T>(Func<IntPtr, T> func)
{
var handle = GetHandleForUse();
bool addedRef = false;
try
{
handle.DangerousAddRef(ref addedRef);
var pointer = handle.DangerousGetHandle();
if (pointer == IntPtr.Zero)
{
throw new ObjectDisposedException(nameof(Engine));
}
return func(pointer);
}
finally
{
if (addedRef)
{
handle.DangerousRelease();
}
}
}
internal T UseHandleForInterop<T>(Func<IntPtr, T> func)
{
return UseHandle(func);
return ResultHelpers.GetStringResult(result);
}
}

View File

@@ -89,12 +89,14 @@ namespace Regorus
);
}
if (result.int_value < 0)
try
{
throw new OverflowException($"{errorContext}: native value was negative ({result.int_value})");
return checked((ulong)result.int_value);
}
catch (OverflowException ex)
{
throw new OverflowException($"{errorContext}: native value was out of range ({result.int_value})", ex);
}
return (ulong)result.int_value;
}
finally
{

View File

@@ -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<Utf8Marshaller.PinnedUtf8> _pins;
private bool _disposed;
internal PinnedPolicyModules(RegorusPolicyModule[] buffer, int length, List<Utf8Marshaller.PinnedUtf8> 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<RegorusPolicyModule>.Shared.Return(Buffer, clearArray: true);
_disposed = true;
}
}
internal sealed class PinnedEntryPoints : IDisposable
{
private readonly List<Utf8Marshaller.PinnedUtf8> _pins;
private bool _disposed;
internal PinnedEntryPoints(IntPtr[] buffer, int length, List<Utf8Marshaller.PinnedUtf8> 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<IntPtr>.Shared.Return(Buffer, clearArray: true);
_disposed = true;
}
}
internal static PinnedPolicyModules PinPolicyModules(IReadOnlyList<PolicyModule> modules)
{
if (modules is null)
{
throw new ArgumentNullException(nameof(modules));
}
var count = modules.Count;
var buffer = ArrayPool<RegorusPolicyModule>.Shared.Rent(count);
var pins = new List<Utf8Marshaller.PinnedUtf8>(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<RegorusPolicyModule>.Shared.Return(buffer, clearArray: true);
throw;
}
}
internal static PinnedEntryPoints PinEntryPoints(IReadOnlyList<string> entryPoints)
{
if (entryPoints is null)
{
throw new ArgumentNullException(nameof(entryPoints));
}
var count = entryPoints.Count;
var buffer = ArrayPool<IntPtr>.Shared.Rent(count);
var pins = new List<Utf8Marshaller.PinnedUtf8>(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<IntPtr>.Shared.Return(buffer, clearArray: true);
throw;
}
}
}
}

View File

@@ -13,14 +13,11 @@ namespace Regorus
/// <summary>
/// Represents a compiled RVM program.
/// </summary>
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));
}
/// <summary>
@@ -36,63 +33,57 @@ namespace Regorus
/// </summary>
public static Program CompileFromModules(string dataJson, IEnumerable<PolicyModule> modules, IEnumerable<string> entryPoints)
{
var modulesArray = modules.ToArray();
var entryPointsArray = entryPoints.ToArray();
if (entryPointsArray.Length == 0)
if (modules is null)
{
throw new ArgumentNullException(nameof(modules));
}
if (entryPoints is null)
{
throw new ArgumentNullException(nameof(entryPoints));
}
return CompileFromModules(dataJson, modules.ToArray(), entryPoints.ToArray());
}
/// <summary>
/// Compile an RVM program from modules and entry points.
/// </summary>
public static Program CompileFromModules(string dataJson, IReadOnlyList<PolicyModule> modules, IReadOnlyList<string> entryPoints)
{
if (modules is null)
{
throw new ArgumentNullException(nameof(modules));
}
if (entryPoints is null)
{
throw new ArgumentNullException(nameof(entryPoints));
}
if (entryPoints.Count == 0)
{
throw new ArgumentException("At least one entry point is required.", nameof(entryPoints));
}
var nativeModules = new RegorusPolicyModule[modulesArray.Length];
var pinnedStrings = new List<Utf8Marshaller.PinnedUtf8>(modulesArray.Length * 2 + entryPointsArray.Length);
var entryPointers = new IntPtr[entryPointsArray.Length];
using var pinnedModules = ModuleMarshalling.PinPolicyModules(modules);
using var pinnedEntryPoints = ModuleMarshalling.PinEntryPoints(entryPoints);
try
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
{
for (int i = 0; i < modulesArray.Length; i++)
fixed (RegorusPolicyModule* modulesPtr = pinnedModules.Buffer)
fixed (IntPtr* entryPtr = pinnedEntryPoints.Buffer)
{
var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id);
var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content);
pinnedStrings.Add(idPinned);
pinnedStrings.Add(contentPinned);
var result = API.regorus_program_compile_from_modules(
(byte*)dataPtr,
modulesPtr,
(UIntPtr)pinnedModules.Length,
(byte**)entryPtr,
(UIntPtr)pinnedEntryPoints.Length);
nativeModules[i] = new RegorusPolicyModule
{
id = idPinned.Pointer,
content = contentPinned.Pointer
};
return GetProgramResult(result);
}
for (int i = 0; i < entryPointsArray.Length; i++)
{
var entryPinned = Utf8Marshaller.Pin(entryPointsArray[i]);
pinnedStrings.Add(entryPinned);
entryPointers[i] = (IntPtr)entryPinned.Pointer;
}
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
{
fixed (RegorusPolicyModule* modulesPtr = nativeModules)
fixed (IntPtr* entryPtr = entryPointers)
{
var result = API.regorus_program_compile_from_modules(
(byte*)dataPtr,
modulesPtr,
(UIntPtr)modulesArray.Length,
(byte**)entryPtr,
(UIntPtr)entryPointsArray.Length);
return GetProgramResult(result);
}
});
}
finally
{
foreach (var pinned in pinnedStrings)
{
pinned.Dispose();
}
}
});
}
/// <summary>
@@ -104,44 +95,48 @@ namespace Regorus
{
throw new ArgumentNullException(nameof(engine));
}
if (entryPoints is null)
{
throw new ArgumentNullException(nameof(entryPoints));
}
var entryPointsArray = entryPoints.ToArray();
if (entryPointsArray.Length == 0)
return CompileFromEngine(engine, entryPoints.ToArray());
}
/// <summary>
/// Compile an RVM program from an engine instance and entry points.
/// </summary>
public static Program CompileFromEngine(Engine engine, IReadOnlyList<string> entryPoints)
{
if (engine is null)
{
throw new ArgumentNullException(nameof(engine));
}
if (entryPoints is null)
{
throw new ArgumentNullException(nameof(entryPoints));
}
if (entryPoints.Count == 0)
{
throw new ArgumentException("At least one entry point is required.", nameof(entryPoints));
}
var pinnedStrings = new List<Utf8Marshaller.PinnedUtf8>(entryPointsArray.Length);
var entryPointers = new IntPtr[entryPointsArray.Length];
try
{
for (int i = 0; i < entryPointsArray.Length; i++)
{
var entryPinned = Utf8Marshaller.Pin(entryPointsArray[i]);
pinnedStrings.Add(entryPinned);
entryPointers[i] = (IntPtr)entryPinned.Pointer;
}
using var pinnedEntryPoints = ModuleMarshalling.PinEntryPoints(entryPoints);
return engine.UseHandleForInterop(enginePtr =>
{
fixed (IntPtr* entryPtr = entryPointers)
{
var result = API.regorus_engine_compile_program_with_entrypoints(
(RegorusEngine*)enginePtr,
(byte**)entryPtr,
(UIntPtr)entryPointsArray.Length);
return GetProgramResult(result);
}
});
}
finally
return engine.UseHandleForInterop(enginePtr =>
{
foreach (var pinned in pinnedStrings)
fixed (IntPtr* entryPtr = pinnedEntryPoints.Buffer)
{
pinned.Dispose();
var result = API.regorus_engine_compile_program_with_entrypoints(
(RegorusEngine*)enginePtr,
(byte**)entryPtr,
(UIntPtr)pinnedEntryPoints.Length);
return GetProgramResult(result);
}
}
});
}
/// <summary>
@@ -169,7 +164,6 @@ namespace Regorus
/// </summary>
public byte[] SerializeBinary()
{
ThrowIfDisposed();
return UseHandle(programPtr =>
{
var result = API.regorus_program_serialize_binary((RegorusProgram*)programPtr);
@@ -182,70 +176,12 @@ namespace Regorus
/// </summary>
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<T>(Func<IntPtr, T> func)
{
var handle = GetHandleForUse();
bool addedRef = false;
try
{
handle.DangerousAddRef(ref addedRef);
var pointer = handle.DangerousGetHandle();
if (pointer == IntPtr.Zero)
{
throw new ObjectDisposedException(nameof(Program));
}
return func(pointer);
}
finally
{
if (addedRef)
{
handle.DangerousRelease();
}
}
}
private static Program GetProgramResult(RegorusResult result)
{
try
@@ -272,27 +208,7 @@ namespace Regorus
private static string? CheckAndDropResult(RegorusResult result)
{
try
{
if (result.status != RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
return result.data_type switch
{
RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output),
RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(),
RegorusDataType.Integer => result.int_value.ToString(),
RegorusDataType.None => null,
_ => Utf8Marshaller.FromUtf8(result.output)
};
}
finally
{
API.regorus_result_drop(result);
}
return ResultHelpers.GetStringResult(result);
}
private static byte[] ExtractBuffer(RegorusResult result)

View File

@@ -8,7 +8,7 @@
<LangVersion>10.0</LangVersion>
<!-- See https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-pack -->
<VersionPrefix>0.9.0</VersionPrefix>
<VersionPrefix>0.9.1</VersionPrefix>
<VersionSuffix>$(VersionSuffix)</VersionSuffix>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT AND Apache-2.0 AND BSD-3-Clause</PackageLicenseExpression>

View File

@@ -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);
}
}
}
}

View File

@@ -7,22 +7,35 @@ using Regorus.Internal;
#nullable enable
namespace Regorus
{
/// <summary>
/// Execution mode for the RVM runtime.
/// </summary>
public enum ExecutionMode : byte
{
/// <summary>
/// Run to completion without yielding.
/// </summary>
RunToCompletion = 0,
/// <summary>
/// Suspendable execution mode.
/// </summary>
Suspendable = 1,
}
/// <summary>
/// Wrapper for the Regorus RVM runtime.
/// </summary>
public unsafe sealed class Rvm : IDisposable
public unsafe sealed class Rvm : SafeHandleWrapper
{
private RegorusRvmHandle? _handle;
private int _isDisposed;
public Rvm()
: base(RegorusRvmHandle.Create(), nameof(Rvm))
{
_handle = RegorusRvmHandle.Create();
}
private Rvm(RegorusRvmHandle handle)
: base(handle, nameof(Rvm))
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
/// <summary>
@@ -47,13 +60,12 @@ namespace Regorus
/// </summary>
public void LoadProgram(Program program)
{
ThrowIfDisposed();
if (program is null)
{
throw new ArgumentNullException(nameof(program));
}
program.UseHandle(programPtr =>
program.UseHandleForInterop(programPtr =>
{
UseHandle(vmPtr =>
{
@@ -69,7 +81,6 @@ namespace Regorus
/// </summary>
public void SetDataJson(string dataJson)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
{
UseHandle(vmPtr =>
@@ -85,7 +96,6 @@ namespace Regorus
/// </summary>
public void SetInputJson(string inputJson)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(inputJson, inputPtr =>
{
UseHandle(vmPtr =>
@@ -101,7 +111,6 @@ namespace Regorus
/// </summary>
public void SetExecutionMode(byte mode)
{
ThrowIfDisposed();
UseHandle(vmPtr =>
{
CheckAndDropResult(API.regorus_rvm_set_execution_mode((RegorusRvm*)vmPtr, mode));
@@ -109,12 +118,19 @@ namespace Regorus
});
}
/// <summary>
/// Set the execution mode.
/// </summary>
public void SetExecutionMode(ExecutionMode mode)
{
SetExecutionMode((byte)mode);
}
/// <summary>
/// Execute the program and return the JSON result.
/// </summary>
public string? Execute()
{
ThrowIfDisposed();
return UseHandle(vmPtr =>
{
return CheckAndDropResult(API.regorus_rvm_execute((RegorusRvm*)vmPtr));
@@ -126,7 +142,6 @@ namespace Regorus
/// </summary>
public string? ExecuteEntryPoint(string entryPoint)
{
ThrowIfDisposed();
return Utf8Marshaller.WithUtf8(entryPoint, entryPtr =>
{
return UseHandle(vmPtr =>
@@ -141,7 +156,6 @@ namespace Regorus
/// </summary>
public string? ExecuteEntryPoint(ulong index)
{
ThrowIfDisposed();
return UseHandle(vmPtr =>
{
return CheckAndDropResult(API.regorus_rvm_execute_entry_point_by_index((RegorusRvm*)vmPtr, (UIntPtr)index));
@@ -153,7 +167,6 @@ namespace Regorus
/// </summary>
public string? Resume(string? resumeValueJson)
{
ThrowIfDisposed();
if (resumeValueJson is null)
{
return UseHandle(vmPtr =>
@@ -176,70 +189,12 @@ namespace Regorus
/// </summary>
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<T>(Func<IntPtr, T> func)
{
var handle = GetHandleForUse();
bool addedRef = false;
try
{
handle.DangerousAddRef(ref addedRef);
var pointer = handle.DangerousGetHandle();
if (pointer == IntPtr.Zero)
{
throw new ObjectDisposedException(nameof(Rvm));
}
return func(pointer);
}
finally
{
if (addedRef)
{
handle.DangerousRelease();
}
}
}
private static Rvm GetRvmResult(RegorusResult result)
{
try
@@ -266,27 +221,7 @@ namespace Regorus
private static string? CheckAndDropResult(RegorusResult result)
{
try
{
if (result.status != RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
return result.data_type switch
{
RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output),
RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(),
RegorusDataType.Integer => result.int_value.ToString(),
RegorusDataType.None => null,
_ => Utf8Marshaller.FromUtf8(result.output)
};
}
finally
{
API.regorus_result_drop(result);
}
return ResultHelpers.GetStringResult(result);
}
}
}

View File

@@ -0,0 +1,272 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Runtime.InteropServices;
#nullable enable
namespace Regorus
{
/// <summary>
/// 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.
/// </summary>
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<IntPtr> action)
{
// Reuse the generic path to keep add/ref/release in one place.
UseHandle<object?>(ptr =>
{
action(ptr);
return null;
});
}
protected T UseHandle<T>(Func<IntPtr, T> 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<T>(Func<IntPtr, T> func)
{
// Explicit alias for interop-specific call sites.
return UseHandle(func);
}
internal void UseHandleForInterop(Action<IntPtr> 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();
}
/// <summary>
/// Tracks in-flight operations and coordinates disposal.
/// </summary>
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();
}
}
}
}

View File

@@ -44,7 +44,7 @@ namespace Regorus
protected override bool ReleaseHandle()
{
if (!IsInvalid && !IsClosed)
if (!IsInvalid)
{
unsafe
{
@@ -76,7 +76,7 @@ namespace Regorus
protected override bool ReleaseHandle()
{
if (!IsInvalid && !IsClosed)
if (!IsInvalid)
{
unsafe
{
@@ -124,7 +124,7 @@ namespace Regorus
protected override bool ReleaseHandle()
{
if (!IsInvalid && !IsClosed)
if (!IsInvalid)
{
unsafe
{
@@ -172,7 +172,7 @@ namespace Regorus
protected override bool ReleaseHandle()
{
if (!IsInvalid && !IsClosed)
if (!IsInvalid)
{
unsafe
{

View File

@@ -2,6 +2,8 @@
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Regorus.Internal;
#nullable enable
@@ -27,7 +29,7 @@ namespace Regorus
{
unsafe
{
CheckAndDropResult(Internal.API.regorus_resource_schema_register((byte*)namePtr, (byte*)schemaPtr));
ResultHelpers.GetStringResult(Internal.API.regorus_resource_schema_register((byte*)namePtr, (byte*)schemaPtr));
}
});
});
@@ -46,7 +48,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_resource_schema_contains((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -61,7 +63,7 @@ namespace Regorus
get
{
var result = Internal.API.regorus_resource_schema_len();
return GetIntResult(result);
return ResultHelpers.GetIntResult(result);
}
}
@@ -75,7 +77,7 @@ namespace Regorus
get
{
var result = Internal.API.regorus_resource_schema_is_empty();
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
}
@@ -86,7 +88,16 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static string ListResourceNames()
{
return CheckAndDropResult(Internal.API.regorus_resource_schema_list_names()) ?? "[]";
return ResultHelpers.GetStringResult(Internal.API.regorus_resource_schema_list_names()) ?? "[]";
}
/// <summary>
/// List all registered resource schema names as managed strings.
/// </summary>
public static IReadOnlyList<string> GetResourceNames()
{
var json = ListResourceNames();
return JsonSerializer.Deserialize<string[]>(json) ?? Array.Empty<string>();
}
/// <summary>
@@ -102,7 +113,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_resource_schema_remove((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -113,7 +124,7 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static void ClearResources()
{
CheckAndDropResult(Internal.API.regorus_resource_schema_clear());
ResultHelpers.GetStringResult(Internal.API.regorus_resource_schema_clear());
}
/// <summary>
@@ -130,7 +141,7 @@ namespace Regorus
{
unsafe
{
CheckAndDropResult(Internal.API.regorus_effect_schema_register((byte*)namePtr, (byte*)schemaPtr));
ResultHelpers.GetStringResult(Internal.API.regorus_effect_schema_register((byte*)namePtr, (byte*)schemaPtr));
}
});
});
@@ -149,7 +160,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_effect_schema_contains((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -164,7 +175,7 @@ namespace Regorus
get
{
var result = Internal.API.regorus_effect_schema_len();
return GetIntResult(result);
return ResultHelpers.GetIntResult(result);
}
}
@@ -178,7 +189,7 @@ namespace Regorus
get
{
var result = Internal.API.regorus_effect_schema_is_empty();
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
}
@@ -189,7 +200,16 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static string ListEffectNames()
{
return CheckAndDropResult(Internal.API.regorus_effect_schema_list_names()) ?? "[]";
return ResultHelpers.GetStringResult(Internal.API.regorus_effect_schema_list_names()) ?? "[]";
}
/// <summary>
/// List all registered effect schema names as managed strings.
/// </summary>
public static IReadOnlyList<string> GetEffectNames()
{
var json = ListEffectNames();
return JsonSerializer.Deserialize<string[]>(json) ?? Array.Empty<string>();
}
/// <summary>
@@ -205,7 +225,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_effect_schema_remove((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -216,68 +236,7 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
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());
}
}
}

View File

@@ -2,6 +2,8 @@
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Regorus.Internal;
#nullable enable
@@ -26,7 +28,7 @@ namespace Regorus
{
unsafe
{
CheckAndDropResult(Internal.API.regorus_register_target_from_json((byte*)targetPtr));
ResultHelpers.GetStringResult(Internal.API.regorus_register_target_from_json((byte*)targetPtr));
}
});
}
@@ -44,7 +46,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_target_registry_contains((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -56,7 +58,16 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static string ListNames()
{
return CheckAndDropResult(Internal.API.regorus_target_registry_list_names()) ?? "[]";
return ResultHelpers.GetStringResult(Internal.API.regorus_target_registry_list_names()) ?? "[]";
}
/// <summary>
/// Get a list of all registered target names as managed strings.
/// </summary>
public static IReadOnlyList<string> GetNames()
{
var json = ListNames();
return JsonSerializer.Deserialize<string[]>(json) ?? Array.Empty<string>();
}
/// <summary>
@@ -72,7 +83,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_target_registry_remove((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -83,7 +94,7 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static void Clear()
{
CheckAndDropResult(Internal.API.regorus_target_registry_clear());
ResultHelpers.GetStringResult(Internal.API.regorus_target_registry_clear());
}
/// <summary>
@@ -96,10 +107,9 @@ namespace Regorus
get
{
var result = Internal.API.regorus_target_registry_len();
return GetIntResult(result);
return ResultHelpers.GetIntResult(result);
}
}
/// <summary>
/// Check if the target registry is empty.
/// </summary>
@@ -110,68 +120,7 @@ namespace Regorus
get
{
var result = Internal.API.regorus_target_registry_is_empty();
return GetBoolResult(result);
}
}
private static string? CheckAndDropResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
return result.data_type switch
{
Internal.RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output),
Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(),
Internal.RegorusDataType.Integer => result.int_value.ToString(),
Internal.RegorusDataType.None => null,
_ => Utf8Marshaller.FromUtf8(result.output)
};
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private static bool GetBoolResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
return result.data_type == Internal.RegorusDataType.Boolean ? result.bool_value : false;
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private static long GetIntResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
return result.data_type == Internal.RegorusDataType.Integer ? result.int_value : 0;
}
finally
{
Internal.API.regorus_result_drop(result);
return ResultHelpers.GetBoolResult(result);
}
}
}

View File

@@ -17,10 +17,10 @@ namespace Regorus.Internal
/// </summary>
internal static class Utf8Marshaller
{
// Mirrors BCL patterns (e.g., System.Text.Json encoding helpers) by stackalloc'ing
// up to 512 bytes to cover common short strings while keeping the stack usage well
// below typical per-frame limits; larger payloads fall back to pooled buffers.
private const int StackAllocThreshold = 512;
// Mirrors BCL patterns (e.g., System.Text.Json encoding helpers) by stackalloc'ing
// up to 512 bytes to cover common short strings while keeping the stack usage well
// below typical per-frame limits; larger payloads fall back to pooled buffers.
private const int StackAllocThreshold = 512;
/// <summary>
/// Represents a pooled and pinned UTF-8 buffer suitable for scenarios where

View File

@@ -65,7 +65,7 @@ triplet_count := count([1 |
private const string EXECUTION_TIMER_QUERY = "data.limits.timer.triplet_count";
private const int EXECUTION_TIMER_VALUE_COUNT = 40;
private const string RVM_POLICY = """
private const string RVM_POLICY = """
package demo
import rego.v1
@@ -78,7 +78,7 @@ allow if {
}
""";
private const string RVM_DATA = """
private const string RVM_DATA = """
{
"roles": {
"alice": ["admin", "reader"]
@@ -86,13 +86,13 @@ allow if {
}
""";
private const string RVM_INPUT = """
private const string RVM_INPUT = """
{
"user": "alice"
}
""";
private const string HOST_AWAIT_POLICY = """
private const string HOST_AWAIT_POLICY = """
package demo
import rego.v1
@@ -105,7 +105,7 @@ allow if {
}
""";
private const string HOST_AWAIT_INPUT = """
private const string HOST_AWAIT_INPUT = """
{
"account": {
"id": "acct-1",
@@ -216,7 +216,7 @@ allow if {
var nonCompliantResult = compiledPolicy.EvalWithInput(NON_COMPLIANT_STORAGE_ACCOUNT);
Console.WriteLine($"Result: {nonCompliantResult}");
// 4. Demonstrate thread-safe concurrent evaluation
Console.WriteLine("\n4. Testing concurrent evaluation from multiple threads:");
DemonstrateConcurrentEvaluation(compiledPolicy);
@@ -246,42 +246,44 @@ allow if {
};
Console.WriteLine($"Starting {testInputs.Length} concurrent evaluations...");
var tasks = testInputs.Select(input =>
Task.Run(() => {
var tasks = testInputs.Select(input =>
Task.Run(() =>
{
var (threadName, json) = input;
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// Multiple evaluations per thread to stress test
var results = new List<string>();
for (int i = 0; i < 1000; i++)
{
var result = compiledPolicy.EvalWithInput(json);
var result = compiledPolicy.EvalWithInput(json)
?? throw new System.InvalidOperationException("Expected EvalWithInput to return a JSON value.");
results.Add(result);
}
stopwatch.Stop();
var microseconds = stopwatch.ElapsedTicks * 1000000 / System.Diagnostics.Stopwatch.Frequency;
// Verify all results are identical (thread safety)
var firstResult = results[0];
var allIdentical = results.All(r => r == firstResult);
Console.WriteLine($"✓ {threadName}: {results.Count} evaluations in {microseconds}μs, " +
$"Results consistent: {allIdentical}");
return (threadName, results.Count, microseconds, allIdentical);
})
).ToArray();
// Wait for all threads to complete
var results = Task.WhenAll(tasks).Result;
Console.WriteLine("\nConcurrency test results:");
var totalEvaluations = results.Sum(r => r.Item2);
var maxTime = results.Max(r => r.Item3);
var allConsistent = results.All(r => r.allIdentical);
Console.WriteLine($"✓ Total evaluations: {totalEvaluations}");
Console.WriteLine($"✓ Max thread time: {maxTime}μs");
Console.WriteLine($"✓ All threads consistent: {allConsistent}");
@@ -292,28 +294,28 @@ allow if {
static void DemonstratePolicyInfo(Regorus.CompiledPolicy compiledPolicy)
{
Console.WriteLine("Getting policy metadata using GetPolicyInfo()...");
try
{
var policyInfo = compiledPolicy.GetPolicyInfo();
Console.WriteLine($"✓ Policy Information Retrieved:");
Console.WriteLine($" Target Name: {policyInfo.TargetName ?? "None"}");
Console.WriteLine($" Effect Rule: {policyInfo.EffectRule ?? "None"}");
Console.WriteLine($" Entrypoint Rule: {policyInfo.EntrypointRule}");
Console.WriteLine($" Module IDs ({policyInfo.ModuleIds.Count}):");
foreach (var moduleId in policyInfo.ModuleIds)
{
Console.WriteLine($" - {moduleId}");
}
Console.WriteLine($" Applicable Resource Types ({policyInfo.ApplicableResourceTypes.Count}):");
foreach (var resourceType in policyInfo.ApplicableResourceTypes)
{
Console.WriteLine($" - {resourceType}");
}
if (policyInfo.Parameters != null && policyInfo.Parameters.Count > 0)
{
Console.WriteLine($" Policy Parameters:");
@@ -333,7 +335,7 @@ allow if {
Console.WriteLine($" Description: {param.Description}");
}
}
if (parameterSet.Modifiers.Count > 0)
{
Console.WriteLine($" Modifiers ({parameterSet.Modifiers.Count}):");
@@ -348,11 +350,11 @@ allow if {
{
Console.WriteLine(" No parameter information available");
}
// Demonstrate JSON serialization of policy info
Console.WriteLine("\n✓ Policy Info as JSON:");
var jsonOptions = new JsonSerializerOptions
{
var jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

View File

@@ -42,7 +42,8 @@ w.Restart();
// Set input and eval rule.
engine.SetInputFromJsonFile("../../../tests/aci/input.json");
var value = engine.EvalRule("data.framework.mount_overlay");
var value = engine.EvalRule("data.framework.mount_overlay")
?? throw new System.InvalidOperationException("Expected EvalRule to return a JSON value.");
#if NET8_0_OR_GREATER
var valueDoc = System.Text.Json.JsonDocument.Parse(value);

View File

@@ -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",

View File

@@ -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"

View File

@@ -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",

View File

@@ -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"

View File

@@ -9,7 +9,7 @@
<groupId>com.microsoft.regorus</groupId>
<artifactId>regorus-java</artifactId>
<version>0.9.0</version>
<version>0.9.1</version>
<name>Regorus Java</name>
<description>Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust</description>

View File

@@ -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",

View File

@@ -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"

View File

@@ -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"

View File

@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Regorus
VERSION = "0.9.0"
VERSION = "0.9.1"
end

View File

@@ -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",

View File

@@ -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"

View File

@@ -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()?;

View File

@@ -308,6 +308,18 @@ pub struct TestCsharpCommand {
/// Restore and test using Regorus NuGet artefacts located at DIR. Defaults to bindings/csharp/Regorus/bin/<configuration>.
#[arg(long = "nuget-dir", value_name = "DIR")]
pub nuget_dir: Option<PathBuf>,
/// Optional dotnet test filter to apply when running Regorus.Tests.
#[arg(long = "test-filter", value_name = "FILTER")]
pub test_filter: Option<String>,
/// 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)?;