mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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:
committed by
GitHub
parent
455d2aa588
commit
96360fa9d8
10
CHANGELOG.md
10
CHANGELOG.md
@@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.9.1](https://github.com/microsoft/regorus/compare/regorus-v0.9.0...regorus-v0.9.1) - 2026-02-06
|
||||
|
||||
### Fixed
|
||||
- Release native C# handles reliably to avoid memory growth ([#571](https://github.com/microsoft/regorus/pull/571)).
|
||||
- Centralize C# handle gating with a short dispose wait and deferred release to avoid leaks while blocking new calls ([#571](https://github.com/microsoft/regorus/pull/571)).
|
||||
|
||||
### Added
|
||||
- Manual C# memory growth tests for both `using` and finalizer paths ([#571](https://github.com/microsoft/regorus/pull/571)).
|
||||
- C# test runner options for filtered tests, console logging, and skipping sample apps ([#571](https://github.com/microsoft/regorus/pull/571)).
|
||||
|
||||
## [0.5.0](https://github.com/microsoft/regorus/compare/regorus-v0.4.0...regorus-v0.5.0) - 2025-07-08
|
||||
|
||||
### Added
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1227,7 +1227,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "regorus"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ===");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
320
bindings/csharp/Regorus.Tests/MemoryGrowthTests.cs
Normal file
320
bindings/csharp/Regorus.Tests/MemoryGrowthTests.cs
Normal 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.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
156
bindings/csharp/Regorus/ModuleMarshalling.cs
Normal file
156
bindings/csharp/Regorus/ModuleMarshalling.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
73
bindings/csharp/Regorus/ResultHelpers.cs
Normal file
73
bindings/csharp/Regorus/ResultHelpers.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
272
bindings/csharp/Regorus/SafeHandleWrapper.cs
Normal file
272
bindings/csharp/Regorus/SafeHandleWrapper.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
4
bindings/ffi/Cargo.lock
generated
4
bindings/ffi/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
4
bindings/java/Cargo.lock
generated
4
bindings/java/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
4
bindings/python/Cargo.lock
generated
4
bindings/python/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Regorus
|
||||
VERSION = "0.9.0"
|
||||
VERSION = "0.9.1"
|
||||
end
|
||||
|
||||
4
bindings/wasm/Cargo.lock
generated
4
bindings/wasm/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
Reference in New Issue
Block a user