Skip to content

Commit fa62435

Browse files
author
Mark Birger
committed
FFI and C# bindings for RVM host-await builtins
Expose registered host-await builtins, Program compilation, and RVM accessors through the FFI layer and C# bindings. FFI (bindings/ffi/src/rvm.rs): - compile_from_modules with host-await builtin registration - set/get host-await responses, argument, identifier - RegorusHostAwaitBuiltin C struct C# (bindings/csharp/Regorus/): - Program.CompileFromModules overloads with HostAwaitBuiltin[] - Rvm.SetHostAwaitResponses, GetHostAwaitArgument, GetHostAwaitIdentifier - HostAwaitBuiltin readonly struct, ExecutionMode enum - ModuleMarshalling: PinnedUtf8Strings, PinnedHostAwaitBuiltins Rust (src/rvm/vm/machine.rs): - get_host_await_argument() and get_host_await_identifier() accessors Docs: README examples, API.md reference, vm-runtime.md accessors Tests: suspendable + run-to-completion C# scenarios
1 parent 2864312 commit fa62435

11 files changed

Lines changed: 769 additions & 75 deletions

File tree

bindings/csharp/API.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,106 @@ var tasks = Enumerable.Range(0, 100).Select(i =>
343343
var results = await Task.WhenAll(tasks);
344344
```
345345

346+
## RVM Classes
347+
348+
### Program
349+
350+
`Program` represents a compiled RVM bytecode artifact. It can be created from
351+
modules+entrypoints or from an `Engine`, serialized to binary, and loaded into
352+
an `Rvm` for execution.
353+
354+
```csharp
355+
public sealed class Program : IDisposable
356+
{
357+
// Compile from modules with entry points
358+
public static Program CompileFromModules(
359+
string dataJson,
360+
IReadOnlyList<PolicyModule> modules,
361+
IReadOnlyList<string> entryPoints);
362+
363+
// Compile with registered host-await builtins
364+
public static Program CompileFromModules(
365+
string dataJson,
366+
IReadOnlyList<PolicyModule> modules,
367+
IReadOnlyList<string> entryPoints,
368+
IReadOnlyList<HostAwaitBuiltin> hostAwaitBuiltins);
369+
370+
// Compile from an Engine instance
371+
public static Program CompileFromEngine(Engine engine, IReadOnlyList<string> entryPoints);
372+
373+
// Serialize/deserialize
374+
public byte[] SerializeBinary();
375+
public static Program DeserializeBinary(byte[] data, out bool isPartial);
376+
377+
// Debug listing
378+
public string GenerateListing();
379+
380+
public void Dispose();
381+
}
382+
```
383+
384+
### Rvm
385+
386+
`Rvm` is the virtual machine that executes a loaded `Program`.
387+
388+
```csharp
389+
public sealed class Rvm : IDisposable
390+
{
391+
// Load a compiled program
392+
public void LoadProgram(Program program);
393+
394+
// Set data and input documents
395+
public void SetDataJson(string dataJson);
396+
public void SetInputJson(string inputJson);
397+
398+
// Configure execution
399+
public void SetExecutionMode(ExecutionMode mode);
400+
401+
// Run
402+
public string? Execute();
403+
public string? ExecuteEntryPoint(string entryPoint);
404+
405+
// Suspendable-mode host-await interaction
406+
public string? Resume(string valueJson);
407+
public string? GetExecutionState();
408+
public string? GetHostAwaitIdentifier();
409+
public string? GetHostAwaitArgument();
410+
411+
// Run-to-completion-mode host-await pre-loading
412+
public void SetHostAwaitResponses(string identifier, string[] valuesJson);
413+
414+
public void Dispose();
415+
}
416+
```
417+
418+
### HostAwaitBuiltin
419+
420+
Declares a function name that the compiler should treat as a host-await call.
421+
When the VM encounters a call to this function, it suspends (suspendable mode)
422+
or consumes a pre-loaded response (run-to-completion mode).
423+
424+
```csharp
425+
public readonly struct HostAwaitBuiltin
426+
{
427+
public string Name { get; }
428+
public int ArgCount { get; }
429+
430+
public HostAwaitBuiltin(string name, int argCount);
431+
}
432+
```
433+
434+
### ExecutionMode
435+
436+
Controls how the VM handles host-await instructions.
437+
438+
```csharp
439+
public enum ExecutionMode
440+
{
441+
RunToCompletion = 0,
442+
Suspendable = 1,
443+
}
444+
```
445+
346446
## Performance Considerations
347447

348448
### Compilation Overhead

bindings/csharp/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,84 @@ var result = vm.Execute();
105105
Console.WriteLine($"allow: {result}");
106106
```
107107

108+
## RVM with Registered Host-Await Builtins
109+
110+
Host-await builtins let you register custom function names at compile time.
111+
When the VM encounters a call to one of these functions, it suspends execution
112+
so the host can resolve the call externally and resume with a value.
113+
114+
### Suspendable mode (resolve one call at a time)
115+
116+
```csharp
117+
using Regorus;
118+
119+
const string Policy = """
120+
package demo
121+
import rego.v1
122+
123+
default allow := false
124+
125+
allow if {
126+
account := get_account({"id": input.account_id})
127+
account.status == "active"
128+
}
129+
""";
130+
131+
var modules = new[] { new PolicyModule("demo.rego", Policy) };
132+
var entryPoints = new[] { "data.demo.allow" };
133+
var builtins = new[] { new HostAwaitBuiltin("get_account", 1) };
134+
135+
using var program = Program.CompileFromModules("{}", modules, entryPoints, builtins);
136+
using var vm = new Rvm();
137+
vm.SetExecutionMode(ExecutionMode.Suspendable);
138+
vm.LoadProgram(program);
139+
vm.SetInputJson("""{"account_id": "acct-42"}""");
140+
141+
// First Execute suspends when get_account() is called
142+
vm.Execute();
143+
144+
// Inspect which builtin suspended and what argument was passed
145+
var identifier = vm.GetHostAwaitIdentifier(); // "get_account"
146+
var argument = vm.GetHostAwaitArgument(); // {"id":"acct-42"}
147+
148+
// Resolve externally, then resume
149+
var result = vm.Resume("""{"status": "active", "name": "Alice"}""");
150+
Console.WriteLine($"allow: {result}"); // true
151+
```
152+
153+
### Run-to-completion mode (pre-load responses)
154+
155+
```csharp
156+
using Regorus;
157+
158+
const string Policy = """
159+
package demo
160+
import rego.v1
161+
162+
default greeting := "unknown"
163+
164+
greeting := msg if {
165+
msg := translate(input.lang)
166+
}
167+
""";
168+
169+
var modules = new[] { new PolicyModule("demo.rego", Policy) };
170+
var entryPoints = new[] { "data.demo.greeting" };
171+
var builtins = new[] { new HostAwaitBuiltin("translate", 1) };
172+
173+
using var program = Program.CompileFromModules("{}", modules, entryPoints, builtins);
174+
using var vm = new Rvm();
175+
vm.SetExecutionMode(ExecutionMode.RunToCompletion);
176+
vm.LoadProgram(program);
177+
vm.SetInputJson("""{"lang": "es"}""");
178+
179+
// Queue responses before execution
180+
vm.SetHostAwaitResponses("translate", new[] { "\"hola\"" });
181+
182+
var result = vm.Execute();
183+
Console.WriteLine($"greeting: {result}"); // "hola"
184+
```
185+
108186
## Azure RBAC Condition Evaluation
109187

110188
Evaluate Azure RBAC condition expressions directly with a JSON evaluation context:

bindings/csharp/Regorus.Tests/RvmProgramTests.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,78 @@ public void Program_host_await_suspend_and_resume_succeeds()
116116
var resumed = vm.Resume("{\"tier\":\"gold\"}");
117117
Assert.AreEqual("true", resumed, "expected allow=true after resume");
118118
}
119+
120+
private const string GetAccountPolicy = """
121+
package demo
122+
import rego.v1
123+
124+
default allow := false
125+
126+
allow if {
127+
account := get_account({"id": input.account_id})
128+
account.status == "active"
129+
}
130+
""";
131+
132+
[TestMethod]
133+
public void RegisteredHostAwait_Suspendable_SuspendAndResume()
134+
{
135+
var modules = new[] { new PolicyModule("account.rego", GetAccountPolicy) };
136+
var entryPoints = new[] { "data.demo.allow" };
137+
var hostAwaitBuiltins = new[] { new HostAwaitBuiltin("get_account", 1) };
138+
139+
using var program = Program.CompileFromModules("{}", modules, entryPoints, hostAwaitBuiltins);
140+
using var vm = new Rvm();
141+
vm.SetExecutionMode(ExecutionMode.Suspendable);
142+
vm.LoadProgram(program);
143+
vm.SetInputJson("{\"account_id\": \"acct-42\"}");
144+
145+
// Execute — should suspend on get_account()
146+
vm.Execute();
147+
148+
// Verify we're suspended due to HostAwait with identifier "get_account"
149+
var identifier = vm.GetHostAwaitIdentifier();
150+
Assert.AreEqual("\"get_account\"", identifier, "expected identifier to be get_account");
151+
152+
var argument = vm.GetHostAwaitArgument();
153+
Assert.IsNotNull(argument, "expected non-null argument");
154+
StringAssert.Contains(argument!, "acct-42", "expected account_id in argument");
155+
156+
// Resume with an account response
157+
var result = vm.Resume("{\"status\": \"active\", \"name\": \"Alice\"}");
158+
Assert.AreEqual("true", result, "expected allow=true after resume");
159+
}
160+
161+
private const string TranslatePolicy = """
162+
package demo
163+
import rego.v1
164+
165+
default greeting := "unknown"
166+
167+
greeting := msg if {
168+
msg := translate(input.lang)
169+
}
170+
""";
171+
172+
[TestMethod]
173+
public void RegisteredHostAwait_RunToCompletion_WithPreloadedResponses()
174+
{
175+
var modules = new[] { new PolicyModule("translate.rego", TranslatePolicy) };
176+
var entryPoints = new[] { "data.demo.greeting" };
177+
var hostAwaitBuiltins = new[] { new HostAwaitBuiltin("translate", 1) };
178+
179+
using var program = Program.CompileFromModules("{}", modules, entryPoints, hostAwaitBuiltins);
180+
using var vm = new Rvm();
181+
vm.SetExecutionMode(ExecutionMode.RunToCompletion);
182+
vm.LoadProgram(program);
183+
vm.SetInputJson("{\"lang\": \"es\"}");
184+
185+
// Pre-load a response for translate
186+
vm.SetHostAwaitResponses("translate", new[] { "\"hola\"" });
187+
188+
// Execute — translate returns "hola"
189+
var result = vm.Execute();
190+
Assert.AreEqual("\"hola\"", result, "expected greeting=hola");
191+
}
192+
119193
}

bindings/csharp/Regorus/Compiler.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,39 @@ public PolicyModule(string id, string content)
3636
}
3737
}
3838

39+
/// <summary>
40+
/// Represents a host-awaitable builtin registration for RVM compilation.
41+
/// When registered, calls to the named function compile to HostAwait instructions
42+
/// rather than regular function calls.
43+
/// </summary>
44+
/// <remarks>
45+
/// Host-await builtins are only supported via the <c>CompileFromModules</c> path.
46+
/// The <c>CompileFromEngine</c> path does not support host-await registration.
47+
/// </remarks>
48+
public readonly struct HostAwaitBuiltin
49+
{
50+
/// <summary>
51+
/// Gets the function name.
52+
/// </summary>
53+
public string Name { get; }
54+
55+
/// <summary>
56+
/// Gets the expected argument count.
57+
/// </summary>
58+
public int ArgCount { get; }
59+
60+
/// <summary>
61+
/// Initializes a new instance of the HostAwaitBuiltin struct.
62+
/// </summary>
63+
/// <param name="name">The function name to register as host-awaitable.</param>
64+
/// <param name="argCount">The expected number of arguments.</param>
65+
public HostAwaitBuiltin(string name, int argCount)
66+
{
67+
Name = name;
68+
ArgCount = argCount;
69+
}
70+
}
71+
3972
/// <summary>
4073
/// Provides static methods for compiling policies into efficient compiled representations.
4174
/// These are convenience methods that create an engine internally and perform compilation.

0 commit comments

Comments
 (0)