Skip to content

Commit 55318c9

Browse files
kushaMark Birger
authored andcommitted
Initial draft of Regorus extension to invoke C# code
1 parent 2858b63 commit 55318c9

File tree

8 files changed

+352
-5
lines changed

8 files changed

+352
-5
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ Cargo.lock
1414
# vscode files
1515
.vscode/
1616

17+
# vs files
18+
.vs/
19+
1720
# worktrees
1821
worktrees/
1922

bindings/csharp/Regorus/Regorus.cs

Lines changed: 161 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,41 @@
44
using System;
55
using System.Runtime.InteropServices;
66
using System.Text;
7+
using System.Collections.Generic;
8+
using System.Text.Json;
79

810

911
#nullable enable
1012
namespace Regorus
1113
{
14+
/// <summary>
15+
/// Delegate for callback functions that can be invoked from Rego policies
16+
/// </summary>
17+
/// <param name="payload">Deserialized JSON object containing the payload from Rego</param>
18+
/// <returns>Object that will be serialized to JSON and converted to a Rego value</returns>
19+
public delegate object RegoCallback(object payload);
20+
1221
public unsafe sealed class Engine : System.IDisposable
1322
{
1423
private Regorus.Internal.RegorusEngine* E;
1524
// Detect redundant Dispose() calls in a thread-safe manner.
1625
// _isDisposed == 0 means Dispose(bool) has not been called yet.
1726
// _isDisposed == 1 means Dispose(bool) has been already called.
1827
private int isDisposed;
28+
29+
// Store callback delegates to prevent garbage collection
30+
private readonly Dictionary<string, (Internal.RegorusCallbackDelegate Delegate, GCHandle Handle)> callbackDelegates
31+
= new Dictionary<string, (Internal.RegorusCallbackDelegate, GCHandle)>();
32+
33+
// Store user callbacks
34+
private readonly Dictionary<string, RegoCallback> callbacks = new Dictionary<string, RegoCallback>();
35+
36+
// JSON serialization options
37+
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
38+
{
39+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
40+
WriteIndented = false
41+
};
1942

2043
public Engine()
2144
{
@@ -51,7 +74,11 @@ void Dispose(bool disposing)
5174
// and unmanaged resources.
5275
if (disposing)
5376
{
54-
// No managed resource to dispose.
77+
// Unregister all callbacks
78+
foreach (var name in new List<string>(callbackDelegates.Keys))
79+
{
80+
UnregisterCallback(name);
81+
}
5582
}
5683

5784
// Call the appropriate methods to clean up
@@ -202,7 +229,139 @@ public void SetGatherPrints(bool enable)
202229
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_take_prints(E));
203230
}
204231

205-
232+
// Generic callback handler that routes to the appropriate user-provided callback
233+
private static unsafe byte* CallbackHandler(byte* payloadPtr, void* contextPtr)
234+
{
235+
try
236+
{
237+
// Context pointer contains the engine instance and callback name
238+
var context = GCHandle.FromIntPtr(new IntPtr(contextPtr));
239+
var contextData = (CallbackContext)context.Target!;
240+
241+
if (contextData == null || contextData.Engine == null)
242+
{
243+
return null;
244+
}
245+
246+
// Convert the payload to a string
247+
var payload = Marshal.PtrToStringUTF8(new IntPtr(payloadPtr));
248+
if (payload == null)
249+
{
250+
return null;
251+
}
252+
253+
// Deserialize the payload to an object
254+
var payloadObject = JsonSerializer.Deserialize<object>(payload, JsonOptions);
255+
if (payloadObject == null)
256+
{
257+
return null;
258+
}
259+
260+
// Get the user callback
261+
if (!contextData.Engine.callbacks.TryGetValue(contextData.CallbackName, out var callback))
262+
{
263+
return null;
264+
}
265+
266+
// Call the user callback
267+
var result = callback(payloadObject);
268+
269+
if (result == null)
270+
{
271+
return null;
272+
}
273+
274+
// Always serialize the result to JSON, even if it's a string
275+
string jsonResult = JsonSerializer.Serialize(result, JsonOptions);
276+
277+
// Convert the result back to a C string that Rust will free
278+
return (byte*)Marshal.StringToCoTaskMemUTF8(jsonResult).ToPointer();
279+
}
280+
catch
281+
{
282+
return null;
283+
}
284+
}
285+
286+
private class CallbackContext
287+
{
288+
public Engine Engine { get; set; }
289+
public string CallbackName { get; set; }
290+
291+
public CallbackContext(Engine engine, string name)
292+
{
293+
Engine = engine;
294+
CallbackName = name;
295+
}
296+
}
297+
298+
/// <summary>
299+
/// Register a callback function that can be invoked from Rego policies
300+
/// </summary>
301+
/// <param name="name">Name of the callback function to register</param>
302+
/// <param name="callback">Callback function to be invoked</param>
303+
/// <returns>True if registration succeeded, otherwise false</returns>
304+
public bool RegisterCallback(string name, RegoCallback callback)
305+
{
306+
if (string.IsNullOrEmpty(name) || callback == null)
307+
{
308+
return false;
309+
}
310+
311+
// Store the callback in our dictionary
312+
callbacks[name] = callback;
313+
314+
// Create a context object and GCHandle
315+
var contextData = new CallbackContext(this, name);
316+
var contextHandle = GCHandle.Alloc(contextData);
317+
var contextPtr = GCHandle.ToIntPtr(contextHandle);
318+
319+
// Create a delegate for the callback handler
320+
var callbackDelegate = new Internal.RegorusCallbackDelegate(CallbackHandler);
321+
322+
// Store the delegate to prevent garbage collection
323+
callbackDelegates[name] = (callbackDelegate, contextHandle);
324+
325+
// Register the callback with the native code
326+
var nameBytes = NullTerminatedUTF8Bytes(name);
327+
fixed (byte* namePtr = nameBytes)
328+
{
329+
var result = Internal.API.regorus_register_callback(namePtr, callbackDelegate, (void*)contextPtr);
330+
return result == Internal.RegorusStatus.RegorusStatusOk;
331+
}
332+
}
333+
334+
/// <summary>
335+
/// Unregister a previously registered callback function
336+
/// </summary>
337+
/// <param name="name">Name of the callback function to unregister</param>
338+
/// <returns>True if unregistration succeeded, otherwise false</returns>
339+
public bool UnregisterCallback(string name)
340+
{
341+
if (string.IsNullOrEmpty(name))
342+
{
343+
return false;
344+
}
345+
346+
// Remove the callback from our dictionary
347+
callbacks.Remove(name);
348+
349+
// Unregister the callback from the native code
350+
var nameBytes = NullTerminatedUTF8Bytes(name);
351+
fixed (byte* namePtr = nameBytes)
352+
{
353+
var result = Internal.API.regorus_unregister_callback(namePtr);
354+
355+
// Free the GCHandle if we have it
356+
if (callbackDelegates.TryGetValue(name, out var delegateInfo))
357+
{
358+
delegateInfo.Handle.Free();
359+
callbackDelegates.Remove(name);
360+
}
361+
362+
return result == Internal.RegorusStatus.RegorusStatusOk;
363+
}
364+
}
206365

207366
string? StringFromUTF8(IntPtr ptr)
208367
{

bindings/csharp/Regorus/Regorus.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
<PackageReadmeFile>README.md</PackageReadmeFile>
1111
</PropertyGroup>
1212

13+
<ItemGroup>
14+
<PackageReference Include="System.Text.Json" Version="9.0.4" />
15+
</ItemGroup>
16+
1317
<!--
1418
$(RegorusFFIArtifactsDir) is the location where regorus shared libraries have been
1519
built for various platforms and copied to. RegorusFFIArtifactsDir is passed in

bindings/csharp/Regorus/RegorusFFI.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010

1111
namespace Regorus.Internal
1212
{
13+
// Add the callback delegate definition
14+
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
15+
internal unsafe delegate byte* RegorusCallbackDelegate(byte* payload, void* context);
16+
1317
internal static unsafe partial class API
1418
{
1519
const string __DllName = "regorus_ffi";
1620

17-
18-
1921
/// <summary>
2022
/// Drop a `RegorusResult`.
2123
///
@@ -192,7 +194,17 @@ internal static unsafe partial class API
192194
[DllImport(__DllName, EntryPoint = "regorus_engine_set_rego_v0", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
193195
internal static extern RegorusResult regorus_engine_set_rego_v0(RegorusEngine* engine, [MarshalAs(UnmanagedType.U1)] bool enable);
194196

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

203+
/// <summary>
204+
/// Unregister a previously registered callback function
205+
/// </summary>
206+
[DllImport(__DllName, EntryPoint = "regorus_unregister_callback", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
207+
internal static extern RegorusStatus regorus_unregister_callback(byte* name);
196208
}
197209

198210
[StructLayout(LayoutKind.Sequential)]

bindings/csharp/scripts/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Steps to run invoke example
2+
3+
1. Install dotnet-script
4+
1. `dotnet build` in `bindings/csharp/Regorus`
5+
1. `cargo build` in `bindings/ffi`
6+
1. Copy `bindings/ffi/target/debug/regorus_ffi.dll` to `bindings/csharp/scripts`
7+
1. `cd` to `bindings/csharp/scripts`
8+
1. Run `dotnet script invoke.csx`

bindings/csharp/scripts/invoke.csx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#r "../Regorus/bin/Debug/netstandard2.1/Regorus.dll"
2+
// No direct reference to regorus_ffi.dll as it's a native DLL
3+
#r "nuget: Newtonsoft.Json, 13.0.2"
4+
#r "nuget: System.Data.Common, 4.3.0"
5+
6+
// Create a new engine
7+
var engine = new Regorus.Engine();
8+
// Register a callback function
9+
bool registerResult = engine.RegisterCallback("test_callback", payload => {
10+
Console.WriteLine($"Called with payload: {payload}");
11+
12+
if (payload is System.Text.Json.JsonElement jsonElement)
13+
{
14+
// Access properties from JsonElement
15+
var testValue = jsonElement.GetProperty("value").GetInt32();
16+
17+
// Return a response object that will be serialized to JSON
18+
return new Dictionary<string, object>
19+
{
20+
["value"] = testValue * 2,
21+
["message"] = "Processing complete"
22+
};
23+
}
24+
25+
return null;
26+
});
27+
28+
Console.WriteLine($"Callback registration result: {registerResult}");
29+
30+
// Add a policy that uses the callback
31+
engine.AddPolicy("example.rego", @"
32+
package example
33+
34+
import future.keywords.if
35+
36+
double_value := invoke(""test_callback"", {""value"": 42}).value
37+
");
38+
39+
// Evaluate query
40+
var result = engine.EvalQuery("data.example.double_value");
41+
Console.WriteLine($"Result: {result}");
42+
43+
// Unregister the callback when done
44+
engine.UnregisterCallback("test_callback");

bindings/ffi/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ custom_allocator = []
2424
[build-dependencies]
2525
cbindgen = "0.28.0"
2626
csbindgen = "=1.9.3"
27+
28+
[dev-dependencies]
29+
csbindgen = "=1.9.3"

0 commit comments

Comments
 (0)