Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Cargo.lock
# vscode files
.vscode/

# vs files
.vs/

# worktrees
worktrees/

Expand All @@ -28,6 +31,7 @@ bindings/ffi/regorus.h
bindings/ffi/regorus.ffi.hpp

bindings/*/target
bindings/csharp/scripts/*.dll

# C# build folders
**bin
Expand Down
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ full-opa = [
"uuid",
"urlquery",
"yaml",

#"rego-extensions"

#"rego-extensions",
#"rego-builtin-extensions"
]

# Features that can be used in no_std environments.
Expand All @@ -86,6 +87,7 @@ opa-no-std = [

# Rego language extensions
rego-extensions = []
rego-builtin-extensions = []

# This feature enables some testing utils for OPA tests.
opa-testutil = []
Expand Down
191 changes: 190 additions & 1 deletion bindings/csharp/Regorus/Regorus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,40 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Collections.Generic;
using System.Text.Json;


#nullable enable
namespace Regorus
{
/// <summary>
/// Delegate for callback functions that can be invoked from Rego policies
/// </summary>
/// <param name="payload">Deserialized JSON object containing the payload from Rego</param>
/// <returns>Object that will be serialized to JSON and converted to a Rego value</returns>
public delegate object RegoCallback(object payload);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: An expectation on the Rust side is that an Engine is efficient to clone. It is much faster to add policies to an engine and clone it many times than creating many instances of the engine and adding policies to them individually.

Cloning an engine will also clone the extension. And a clone could be used from another thread. This means that the extension must be stateless or must be safe to be called from multiple threads.


public unsafe sealed class Engine : System.IDisposable
{
private Regorus.Internal.RegorusEngine* E;
// Detect redundant Dispose() calls in a thread-safe manner.
// _isDisposed == 0 means Dispose(bool) has not been called yet.
// _isDisposed == 1 means Dispose(bool) has been already called.
private int isDisposed;

// Store callback delegates to prevent garbage collection
private readonly Dictionary<string, GCHandle> callbackHandles = new Dictionary<string, GCHandle>();

// Store user callbacks
private readonly Dictionary<string, RegoCallback> callbacks = new Dictionary<string, RegoCallback>();

// JSON serialization options
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};

public Engine()
{
Expand Down Expand Up @@ -51,7 +73,11 @@ void Dispose(bool disposing)
// and unmanaged resources.
if (disposing)
{
// No managed resource to dispose.
// Unregister all callbacks
foreach (var name in new List<string>(callbackHandles.Keys))
{
UnregisterCallback(name);
}
}

// Call the appropriate methods to clean up
Expand Down Expand Up @@ -202,7 +228,170 @@ public void SetGatherPrints(bool enable)
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_take_prints(E));
}

/// <summary>
/// Enable a builtin extension by name
/// </summary>
/// <param name="name">The name of the builtin extension to enable</param>
/// <returns>True if the operation succeeded, otherwise false</returns>
public bool EnableBuiltinExtension(string name)
{
try
{
var nameBytes = NullTerminatedUTF8Bytes(name);
fixed (byte* namePtr = nameBytes)
{
CheckAndDropResult(Internal.API.regorus_engine_enable_builtin_extension(E, namePtr));
return true;
}
}
catch
{
return false;
}
}

/// <summary>
/// Enable the invoke capability to allow Rego policies to call registered callbacks
/// </summary>
/// <returns>True if the operation succeeded, otherwise false</returns>
public bool EnableInvoke()
{
return EnableBuiltinExtension("invoke");
}

// Generate a closure that wraps the user's callback function
private static Internal.RegorusCallbackDelegate GenerateRegorusCallback(RegoCallback callback)
{
return delegate (byte* payloadPtr, void* contextPtr)
{
try
{
// Convert the payload to a string
#if NETSTANDARD2_1
var payload = Marshal.PtrToStringUTF8(new IntPtr(payloadPtr));
#else
var payload = StringFromUTF8Raw(new IntPtr(payloadPtr));
#endif
if (payload == null)
{
return null;
}

// Deserialize the payload to an object
var payloadObject = JsonSerializer.Deserialize<object>(payload, JsonOptions);
if (payloadObject == null)
{
return null;
}

// Call the user's callback function
var result = callback(payloadObject);

if (result == null)
{
return null;
}

// Always serialize the result to JSON, even if it's a string
string jsonResult = JsonSerializer.Serialize(result, JsonOptions);

// Convert the result back to a C string that Rust will free
#if NETSTANDARD2_1
return (byte*)Marshal.StringToCoTaskMemUTF8(jsonResult).ToPointer();
#else
return StringToCoTaskMemUTF8Raw(jsonResult);
#endif
}
catch
{
return null;
}
};
}

// Helper for .NET Standard 2.0 to convert string to UTF8 allocated memory
private static byte* StringToCoTaskMemUTF8Raw(string str)
{
if (str == null)
return null;

var bytes = Encoding.UTF8.GetBytes(str);
var ptr = Marshal.AllocCoTaskMem(bytes.Length + 1);
Marshal.Copy(bytes, 0, ptr, bytes.Length);
Marshal.WriteByte(ptr, bytes.Length, 0);
return (byte*)ptr.ToPointer();
}

// Helper for .NET Standard 2.0 to convert UTF8 to string
private static string StringFromUTF8Raw(IntPtr ptr)
{
if (ptr == IntPtr.Zero)
return null;

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

public bool RegisterCallback(string name, RegoCallback callback)
{
if (string.IsNullOrEmpty(name) || callback == null)
{
return false;
}

// Store the callback in our dictionary
callbacks[name] = callback;

// Generate a closure for this callback
var callbackDelegate = GenerateRegorusCallback(callback);

// Create a GCHandle to prevent garbage collection
var handle = GCHandle.Alloc(callbackDelegate);
callbackHandles[name] = handle;

// Register the callback with the native code
var nameBytes = NullTerminatedUTF8Bytes(name);
fixed (byte* namePtr = nameBytes)
{
var result = Internal.API.regorus_register_callback(namePtr, callbackDelegate, (void*)IntPtr.Zero);
return result == Internal.RegorusStatus.RegorusStatusOk;
}
}

/// <summary>
/// Unregister a previously registered callback function
/// </summary>
/// <param name="name">Name of the callback function to unregister</param>
/// <returns>True if unregistration succeeded, otherwise false</returns>
public bool UnregisterCallback(string name)
{
if (string.IsNullOrEmpty(name))
{
return false;
}

// Remove the callback from our dictionary
callbacks.Remove(name);

// Unregister the callback from the native code
var nameBytes = NullTerminatedUTF8Bytes(name);
fixed (byte* namePtr = nameBytes)
{
var result = Internal.API.regorus_unregister_callback(namePtr);

// Free the GCHandle if we have it
if (callbackHandles.TryGetValue(name, out var handle))
{
handle.Free();
callbackHandles.Remove(name);
}

return result == Internal.RegorusStatus.RegorusStatusOk;
}
}

string? StringFromUTF8(IntPtr ptr)
{
Expand Down
4 changes: 4 additions & 0 deletions bindings/csharp/Regorus/Regorus.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" Version="9.0.4" />
</ItemGroup>

<!--
$(RegorusFFIArtifactsDir) is the location where regorus shared libraries have been
built for various platforms and copied to. RegorusFFIArtifactsDir is passed in
Expand Down
22 changes: 20 additions & 2 deletions bindings/csharp/Regorus/RegorusFFI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@

namespace Regorus.Internal
{
// Add the callback delegate definition
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal unsafe delegate byte* RegorusCallbackDelegate(byte* payload, void* context);

internal static unsafe partial class API
{
const string __DllName = "regorus_ffi";



/// <summary>
/// Drop a `RegorusResult`.
///
Expand Down Expand Up @@ -192,7 +194,23 @@ internal static unsafe partial class API
[DllImport(__DllName, EntryPoint = "regorus_engine_set_rego_v0", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_rego_v0(RegorusEngine* engine, [MarshalAs(UnmanagedType.U1)] bool enable);

/// <summary>
/// Register a callback function that can be called from Rego policies
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_register_callback", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusStatus regorus_register_callback(byte* name, RegorusCallbackDelegate callback, void* context);

/// <summary>
/// Unregister a previously registered callback function
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_unregister_callback", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusStatus regorus_unregister_callback(byte* name);

/// <summary>
/// Enable a builtin extension by name
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_enable_builtin_extension", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_enable_builtin_extension(RegorusEngine* engine, byte* name);
}

[StructLayout(LayoutKind.Sequential)]
Expand Down
13 changes: 13 additions & 0 deletions bindings/csharp/scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Steps to run invoke example

1. Install dotnet-script
1. `dotnet build` in `bindings/csharp/Regorus`
1. `cargo build --release` in `bindings/ffi`
1. Copy `bindings/ffi/target/debug/regorus_ffi.dll` to `bindings/csharp/scripts`
1. `cd` to `bindings/csharp/scripts`
1. Run `dotnet script invoke.csx`

All at once as a single command:
```
cd bindings/csharp/Regorus && dotnet build && cd ../../.. && cd bindings/ffi && cargo build --release --features rego-extensions,rego-builtin-extensions && cd ../.. && cp bindings/ffi/target/release/regorus_ffi.dll bindings/csharp/scripts && cd bindings/csharp/scripts && dotnet script invoke.csx && cd ../../..
```
49 changes: 49 additions & 0 deletions bindings/csharp/scripts/invoke.csx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#r "../Regorus/bin/Debug/netstandard2.1/Regorus.dll"
// No direct reference to regorus_ffi.dll as it's a native DLL
#r "nuget: Newtonsoft.Json, 13.0.2"
#r "nuget: System.Data.Common, 4.3.0"

// Create a new engine
var engine = new Regorus.Engine();

// Enable the invoke extension
bool enableResult = engine.EnableInvoke();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To catch regressions, I'd prefer a test over a script.

Console.WriteLine($"Invoke extension enabled: {enableResult}");

// Register a callback function
bool registerResult = engine.RegisterCallback("test_callback", payload => {
Console.WriteLine($"Called with payload: {payload}");

if (payload is System.Text.Json.JsonElement jsonElement)
{
// Access properties from JsonElement
var testValue = jsonElement.GetProperty("value").GetInt32();

// Return a response object that will be serialized to JSON
return new Dictionary<string, object>
{
["value"] = testValue * 2,
["message"] = "Processing complete"
};
}

return null;
});

Console.WriteLine($"Callback registration result: {registerResult}");

// Add a policy that uses the callback
engine.AddPolicy("example.rego", @"
package example

import future.keywords.if

double_value := invoke(""test_callback"", {""value"": 42}).value
");

// Evaluate query
var result = engine.EvalQuery("data.example.double_value");
Console.WriteLine($"Result: {result}");

// Unregister the callback when done
engine.UnregisterCallback("test_callback");
7 changes: 6 additions & 1 deletion bindings/ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ regorus = { path = "../..", default-features = false }
serde_json = "1.0.140"

[features]
default = ["ast", "std", "coverage", "regorus/arc", "regorus/full-opa"]
default = ["ast", "std", "coverage", "regorus/arc", "regorus/full-opa", "regorus/rego-extensions", "regorus/rego-builtin-extensions"]
ast = ["regorus/ast"]
std = ["regorus/std"]
coverage = ["regorus/coverage"]
custom_allocator = []
rego-extensions = ["regorus/rego-extensions"]
rego-builtin-extensions = ["regorus/rego-builtin-extensions"]

[build-dependencies]
cbindgen = "0.28.0"
csbindgen = "=1.9.3"

[dev-dependencies]
csbindgen = "=1.9.3"
Loading