feat(bindings)!: add RVM/Program support across FFI and language bindings (#565)

- FFI: add RVM/Program APIs, execution state accessors, HostAwait handling, and buffer/result helpers in rvm.rs, common.rs, engine.rs.
- Compiler: emit HostAwait for __builtin_host_await in function_calls.rs.
- RVM tests: add HostAwait regression cases and extend harness for suspend/resume responses in host_await.yaml and mod.rs.
- C/C++: add RVM tests/examples and wrapper updates in rvm_tests.c, rvm_tests.cpp, regorus.hpp, plus CMake wiring.
- C#: add Program/Rvm bindings, SafeHandle/PInvoke, tests, and example usage in Regorus, RvmProgramTests.cs, Program.cs, and README updates.
- Go: add Program/Rvm bindings, tests, and examples in rvm.go, rvm_test.go, main.go.
- Java: add Program/Rvm bindings, JNI glue, and examples in lib.rs, regorus, Test.java.
- Python: add Program/Rvm bindings and examples in lib.rs, test.py.
- WASM: add Program/Rvm bindings and examples in lib.rs, test.js.
- Tooling: wire binding tests in xtask and ignore generated Java artifacts in .gitignore.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2026-01-30 23:55:31 +05:30
committed by GitHub
parent 0316ccd90c
commit 3f7a5496dc
50 changed files with 4990 additions and 110 deletions

View File

@@ -13,6 +13,7 @@ FetchContent_Declare(
FetchContent_MakeAvailable(Corrosion)
project("regorus-test")
enable_testing()
corrosion_import_crate(
# Path to <regorus-source-folder>/bindings/ffi/Cargo.toml
@@ -35,3 +36,10 @@ add_executable(regorus_test main.c)
# Add path to <regorus-source-folder>/bindings/ffi
target_include_directories(regorus_test PRIVATE "../ffi")
target_link_libraries(regorus_test regorus_ffi)
add_executable(regorus_rvm_test rvm_tests.c)
target_include_directories(regorus_rvm_test PRIVATE "../ffi")
target_link_libraries(regorus_rvm_test regorus_ffi)
add_test(NAME regorus_c_engine COMMAND regorus_test)
add_test(NAME regorus_c_rvm COMMAND regorus_rvm_test)

289
bindings/c/rvm_tests.c Normal file
View File

@@ -0,0 +1,289 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include <stdio.h>
#include <string.h>
#include "regorus.h"
static int assert_ok(RegorusResult r, const char* message) {
if (r.status != Ok) {
fprintf(stderr, "%s: %s\n", message, r.error_message ? r.error_message : "(no error)");
return 0;
}
return 1;
}
int main() {
RegorusResult result = {0};
bool result_valid = false;
RegorusProgram* program = NULL;
RegorusBuffer* buffer = NULL;
RegorusProgram* program2 = NULL;
RegorusRvm* vm = NULL;
RegorusProgram* host_program = NULL;
RegorusRvm* host_vm = NULL;
bool is_partial = false;
int exit_code = 1;
const char* data_json =
"{"
" \"roles\": {"
" \"alice\": [\"admin\", \"reader\"]"
" }"
"}";
const char* input_json =
"{"
" \"user\": \"alice\","
" \"actions\": [\"read\"]"
"}";
const char* module_text =
"package demo\n"
"default allow = false\n"
"allow if {\n"
" input.user == \"alice\"\n"
" some role in data.roles[input.user]\n"
" role == \"admin\"\n"
" count(input.actions) > 0\n"
"}\n";
const char* host_data_json = "{}";
const char* host_input_json = "{\"account\":{\"id\":\"acct-1\",\"active\":true}}";
const char* host_module_text =
"package demo\n"
"import rego.v1\n"
"default allow := false\n"
"allow if {\n"
" input.account.active == true\n"
" details := __builtin_host_await(input.account.id, \"account\")\n"
" details.tier == \"gold\"\n"
"}\n";
RegorusPolicyModule module;
module.id = "demo.rego";
module.content = module_text;
const char* entry_points[] = {"data.demo.allow"};
printf("Rego policy:\n%s\n", module_text);
printf("Compiling program from modules...\n");
result = regorus_program_compile_from_modules(
data_json,
&module,
1,
entry_points,
1
);
result_valid = true;
if (!assert_ok(result, "compile program")) {
goto Cleanup;
}
program = (RegorusProgram*)result.pointer_value;
regorus_result_drop(result);
result_valid = false;
printf("Generating assembly listing...\n");
result = regorus_program_generate_listing(program);
result_valid = true;
if (!assert_ok(result, "generate listing")) {
goto Cleanup;
}
printf("Assembly listing:\n%s\n", result.output ? result.output : "(null)");
regorus_result_drop(result);
result_valid = false;
printf("Serializing program...\n");
result = regorus_program_serialize_binary(program);
result_valid = true;
if (!assert_ok(result, "serialize program")) {
goto Cleanup;
}
buffer = (RegorusBuffer*)result.pointer_value;
regorus_result_drop(result);
result_valid = false;
printf("Deserializing program (%zu bytes)...\n", buffer->len);
result = regorus_program_deserialize_binary(
buffer->data,
buffer->len,
&is_partial
);
result_valid = true;
if (!assert_ok(result, "deserialize program")) {
goto Cleanup;
}
if (is_partial) {
fprintf(stderr, "deserialized program marked partial\n");
goto Cleanup;
}
program2 = (RegorusProgram*)result.pointer_value;
regorus_result_drop(result);
result_valid = false;
printf("Creating VM...\n");
vm = regorus_rvm_new();
if (!vm) {
fprintf(stderr, "failed to allocate VM\n");
goto Cleanup;
}
printf("Loading program into VM...\n");
result = regorus_rvm_load_program(vm, program2);
result_valid = true;
if (!assert_ok(result, "load program")) {
goto Cleanup;
}
regorus_result_drop(result);
result_valid = false;
printf("Setting data...\n");
result = regorus_rvm_set_data(vm, data_json);
result_valid = true;
if (!assert_ok(result, "set data")) {
goto Cleanup;
}
regorus_result_drop(result);
result_valid = false;
printf("Setting input...\n");
result = regorus_rvm_set_input(vm, input_json);
result_valid = true;
if (!assert_ok(result, "set input")) {
goto Cleanup;
}
regorus_result_drop(result);
result_valid = false;
printf("Executing entry point...\n");
result = regorus_rvm_execute(vm);
result_valid = true;
if (!assert_ok(result, "execute")) {
goto Cleanup;
}
printf("Execution result (data.demo.allow): %s\n",
result.output ? result.output : "(null)");
printf("Decision: user=alice action=read -> allow=%s\n",
result.output ? result.output : "(null)");
if (!result.output || strcmp(result.output, "true") != 0) {
fprintf(stderr, "unexpected result: %s\n", result.output);
goto Cleanup;
}
printf("\n--- HostAwait example (suspendable execution) ---\n");
RegorusPolicyModule host_module;
host_module.id = "host_await.rego";
host_module.content = host_module_text;
const char* host_entry_points[] = {"data.demo.allow"};
result = regorus_program_compile_from_modules(
host_data_json,
&host_module,
1,
host_entry_points,
1
);
result_valid = true;
if (!assert_ok(result, "compile host await program")) {
goto Cleanup;
}
host_program = (RegorusProgram*)result.pointer_value;
regorus_result_drop(result);
result_valid = false;
host_vm = regorus_rvm_new();
if (!host_vm) {
fprintf(stderr, "failed to allocate host await VM\n");
goto Cleanup;
}
result = regorus_rvm_set_execution_mode(host_vm, 1);
result_valid = true;
if (!assert_ok(result, "set execution mode")) {
goto Cleanup;
}
regorus_result_drop(result);
result_valid = false;
result = regorus_rvm_load_program(host_vm, host_program);
result_valid = true;
if (!assert_ok(result, "load host await program")) {
goto Cleanup;
}
regorus_result_drop(result);
result_valid = false;
result = regorus_rvm_set_data(host_vm, host_data_json);
result_valid = true;
if (!assert_ok(result, "set host data")) {
goto Cleanup;
}
regorus_result_drop(result);
result_valid = false;
result = regorus_rvm_set_input(host_vm, host_input_json);
result_valid = true;
if (!assert_ok(result, "set host input")) {
goto Cleanup;
}
regorus_result_drop(result);
result_valid = false;
result = regorus_rvm_execute(host_vm);
result_valid = true;
if (!assert_ok(result, "execute host await")) {
goto Cleanup;
}
printf("HostAwait initial result: %s\n", result.output ? result.output : "(null)");
regorus_result_drop(result);
result_valid = false;
result = regorus_rvm_get_execution_state(host_vm);
result_valid = true;
if (!assert_ok(result, "get execution state")) {
goto Cleanup;
}
printf("Execution state: %s\n", result.output ? result.output : "(null)");
regorus_result_drop(result);
result_valid = false;
result = regorus_rvm_resume(host_vm, "{\"tier\":\"gold\"}", true);
result_valid = true;
if (!assert_ok(result, "resume host await")) {
goto Cleanup;
}
printf("HostAwait resumed result: %s\n", result.output ? result.output : "(null)");
if (!result.output || strcmp(result.output, "true") != 0) {
fprintf(stderr, "unexpected host await result\n");
goto Cleanup;
}
regorus_result_drop(result);
result_valid = false;
exit_code = 0;
Cleanup:
if (result_valid) {
regorus_result_drop(result);
}
if (host_vm) {
regorus_rvm_drop(host_vm);
}
if (host_program) {
regorus_program_drop(host_program);
}
if (vm) {
regorus_rvm_drop(vm);
}
if (program2) {
regorus_program_drop(program2);
}
if (buffer) {
regorus_buffer_drop(buffer);
}
if (program) {
regorus_program_drop(program);
}
return exit_code;
}

View File

@@ -14,6 +14,7 @@ FetchContent_MakeAvailable(Corrosion)
project("regorus-test")
set(CMAKE_CXX_STANDARD 17)
enable_testing()
# installable ffi target
@@ -83,3 +84,9 @@ install(FILES
add_executable(regorus_test main.cpp)
target_link_libraries(regorus_test regorus_ffi::regorus_ffi)
add_executable(regorus_rvm_test rvm_tests.cpp)
target_link_libraries(regorus_rvm_test regorus_ffi::regorus_ffi)
add_test(NAME regorus_cpp_engine COMMAND regorus_test)
add_test(NAME regorus_cpp_rvm COMMAND regorus_rvm_test)

View File

@@ -1,6 +1,8 @@
#ifndef REGORUS_WRAPPER_HPP
#define REGORUS_WRAPPER_HPP
#include <cstddef>
#include <cstdint>
#include <memory>
#include <variant>
@@ -8,8 +10,11 @@
namespace regorus {
class Result {
public:
class Buffer;
class Program;
class Result {
public:
operator bool() const { return result.status == RegorusStatus::Ok; }
bool operator !() const { return result.status != RegorusStatus::Ok; }
@@ -30,18 +35,39 @@ namespace regorus {
}
}
void* pointer() const {
return result.pointer_value;
}
Program program() const;
Buffer buffer() const;
Result(RegorusResult r) : result(r) {}
Result(Result&& other) noexcept : result(other.result) {
other.result.output = nullptr;
other.result.error_message = nullptr;
other.result.pointer_value = nullptr;
}
Result& operator=(Result&& other) noexcept {
if (this != &other) {
regorus_result_drop(result);
result = other.result;
other.result.output = nullptr;
other.result.error_message = nullptr;
other.result.pointer_value = nullptr;
}
return *this;
}
~Result() {
regorus_result_drop(result);
}
private:
friend class Engine;
RegorusResult result;
Result(RegorusResult r) : result(r) {}
private:
Result(const Result&) = delete;
Result(Result&&) = delete;
Result& operator=(const Result&) = delete;
};
@@ -109,6 +135,10 @@ namespace regorus {
~Engine() {
regorus_engine_drop(engine);
}
RegorusEngine* raw() const {
return engine;
}
private:
@@ -119,6 +149,247 @@ namespace regorus {
Engine(Engine&&) = delete;
Engine& operator=(const Engine&) = delete;
};
class CompiledPolicy {
public:
explicit CompiledPolicy(RegorusCompiledPolicy* p) : policy(p) {}
Result eval_with_input(const char* input_json) {
return Result(regorus_compiled_policy_eval_with_input(policy, input_json));
}
Result get_policy_info() {
return Result(regorus_compiled_policy_get_policy_info(policy));
}
RegorusCompiledPolicy* raw() const {
return policy;
}
~CompiledPolicy() {
if (policy) {
regorus_compiled_policy_drop(policy);
}
}
private:
RegorusCompiledPolicy* policy;
CompiledPolicy(const CompiledPolicy&) = delete;
CompiledPolicy(CompiledPolicy&&) = delete;
CompiledPolicy& operator=(const CompiledPolicy&) = delete;
};
class Buffer {
public:
Buffer() : buffer(nullptr) {}
explicit Buffer(RegorusBuffer* b) : buffer(b) {}
const std::uint8_t* data() const {
return buffer ? buffer->data : nullptr;
}
size_t size() const {
return buffer ? buffer->len : 0;
}
RegorusBuffer* raw() const {
return buffer;
}
~Buffer() {
if (buffer) {
regorus_buffer_drop(buffer);
}
}
private:
RegorusBuffer* buffer;
Buffer(const Buffer&) = delete;
Buffer(Buffer&&) = delete;
Buffer& operator=(const Buffer&) = delete;
};
class Program {
public:
Program() : program(regorus_program_new()) {}
explicit Program(RegorusProgram* p) : program(p) {}
static Result compile_from_policy(
RegorusCompiledPolicy* compiled_policy,
const char* const* entry_points,
size_t entry_points_len
) {
return Result(regorus_program_compile_from_policy(
compiled_policy,
entry_points,
entry_points_len
));
}
static Result compile_from_modules(
const char* data_json,
const RegorusPolicyModule* modules,
size_t modules_len,
const char* const* entry_points,
size_t entry_points_len
) {
return Result(regorus_program_compile_from_modules(
data_json,
modules,
modules_len,
entry_points,
entry_points_len
));
}
static Result compile_from_engine(
RegorusEngine* engine,
const char* const* entry_points,
size_t entry_points_len
) {
return Result(regorus_engine_compile_program_with_entrypoints(
engine,
entry_points,
entry_points_len
));
}
Result serialize_binary() const {
return Result(regorus_program_serialize_binary(program));
}
static Result deserialize_binary(
const std::uint8_t* data,
size_t len,
bool* is_partial
) {
return Result(regorus_program_deserialize_binary(data, len, is_partial));
}
Result generate_listing() const {
return Result(regorus_program_generate_listing(program));
}
Result generate_tabular_listing() const {
return Result(regorus_program_generate_tabular_listing(program));
}
RegorusProgram* raw() const {
return program;
}
~Program() {
if (program) {
regorus_program_drop(program);
}
}
private:
RegorusProgram* program;
Program(const Program&) = delete;
Program(Program&&) = delete;
Program& operator=(const Program&) = delete;
};
inline Program Result::program() const {
return Program(reinterpret_cast<RegorusProgram*>(result.pointer_value));
}
inline Buffer Result::buffer() const {
return Buffer(reinterpret_cast<RegorusBuffer*>(result.pointer_value));
}
class Rvm {
public:
Rvm() : vm(regorus_rvm_new()) {}
explicit Rvm(RegorusRvm* v) : vm(v) {}
static Result create_with_policy(RegorusCompiledPolicy* compiled_policy) {
return Result(regorus_rvm_new_with_policy(compiled_policy));
}
Result load_program(const Program& program) {
return Result(regorus_rvm_load_program(vm, program.raw()));
}
Result set_data(const char* data_json) {
return Result(regorus_rvm_set_data(vm, data_json));
}
Result set_input(const char* input_json) {
return Result(regorus_rvm_set_input(vm, input_json));
}
Result set_max_instructions(size_t max_instructions) {
return Result(regorus_rvm_set_max_instructions(vm, max_instructions));
}
Result set_strict_builtin_errors(bool strict) {
return Result(regorus_rvm_set_strict_builtin_errors(vm, strict));
}
Result set_execution_mode(std::uint8_t mode) {
return Result(regorus_rvm_set_execution_mode(vm, mode));
}
Result set_step_mode(bool enabled) {
return Result(regorus_rvm_set_step_mode(vm, enabled));
}
Result set_execution_timer_config(bool has_config, RegorusExecutionTimerConfig config) {
return Result(regorus_rvm_set_execution_timer_config(vm, has_config, config));
}
Result execute() {
return Result(regorus_rvm_execute(vm));
}
Result execute_entry_point_by_name(const char* entry_point) {
return Result(regorus_rvm_execute_entry_point_by_name(vm, entry_point));
}
Result execute_entry_point_by_index(size_t index) {
return Result(regorus_rvm_execute_entry_point_by_index(vm, index));
}
Result resume(const char* resume_value_json, bool has_value) {
return Result(regorus_rvm_resume(vm, resume_value_json, has_value));
}
Result get_execution_state() {
return Result(regorus_rvm_get_execution_state(vm));
}
RegorusRvm* raw() const {
return vm;
}
~Rvm() {
if (vm) {
regorus_rvm_drop(vm);
}
}
private:
RegorusRvm* vm;
Rvm(const Rvm&) = delete;
Rvm(Rvm&&) = delete;
Rvm& operator=(const Rvm&) = delete;
};
inline Result compile_policy_with_entrypoint(
const char* data_json,
const RegorusPolicyModule* modules,
size_t modules_len,
const char* entry_point
) {
return Result(regorus_compile_policy_with_entrypoint(
data_json,
modules,
modules_len,
entry_point
));
}
}
#endif // REGORUS_WRAPPER_HPP

261
bindings/cpp/rvm_tests.cpp Normal file
View File

@@ -0,0 +1,261 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include <iostream>
#include <string>
#include "regorus.hpp"
int main() {
const char* data_json =
"{"
" \"roles\": {"
" \"alice\": [\"admin\", \"reader\"]"
" }"
"}";
const char* input_json =
"{"
" \"user\": \"alice\","
" \"actions\": [\"read\"]"
"}";
const char* module_text =
"package demo\n"
"default allow = false\n"
"allow if {\n"
" input.user == \"alice\"\n"
" some role in data.roles[input.user]\n"
" role == \"admin\"\n"
" count(input.actions) > 0\n"
"}\n";
const char* host_data_json = "{}";
const char* host_input_json = "{\"account\":{\"id\":\"acct-1\",\"active\":true}}";
const char* host_module_text =
"package demo\n"
"import rego.v1\n"
"default allow := false\n"
"allow if {\n"
" input.account.active == true\n"
" details := __builtin_host_await(input.account.id, \"account\")\n"
" details.tier == \"gold\"\n"
"}\n";
RegorusPolicyModule module;
module.id = "demo.rego";
module.content = module_text;
const char* entry_points[] = {"data.demo.allow"};
std::cout << "Rego policy:\n" << module_text << std::endl;
std::cout << "Compiling program from modules..." << std::endl;
auto program_result = regorus::Program::compile_from_modules(
data_json,
&module,
1,
entry_points,
1
);
if (!program_result) {
std::cerr << "compile program (modules): " << program_result.error() << std::endl;
return 1;
}
regorus::Program program = program_result.program();
std::cout << "Generating assembly listing..." << std::endl;
auto listing_result = program.generate_listing();
if (!listing_result) {
std::cerr << "generate listing: " << listing_result.error() << std::endl;
return 1;
}
std::cout << "Assembly listing:\n" << listing_result.output() << std::endl;
std::cout << "Serializing program..." << std::endl;
auto serialize_result = program.serialize_binary();
if (!serialize_result) {
std::cerr << "serialize program: " << serialize_result.error() << std::endl;
return 1;
}
regorus::Buffer buffer(reinterpret_cast<RegorusBuffer*>(serialize_result.pointer()));
bool is_partial = false;
std::cout << "Deserializing program (" << buffer.size() << " bytes)..." << std::endl;
auto deserialize_result = regorus::Program::deserialize_binary(
buffer.data(),
buffer.size(),
&is_partial
);
if (!deserialize_result) {
std::cerr << "deserialize program: " << deserialize_result.error() << std::endl;
return 1;
}
if (is_partial) {
std::cerr << "deserialized program marked partial" << std::endl;
return 1;
}
regorus::Program program2 = deserialize_result.program();
{
std::cout << "Creating VM..." << std::endl;
regorus::Rvm vm;
auto load_result = vm.load_program(program2);
if (!load_result) {
std::cerr << "load program: " << load_result.error() << std::endl;
return 1;
}
std::cout << "Setting data..." << std::endl;
auto data_result = vm.set_data(data_json);
if (!data_result) {
std::cerr << "set data: " << data_result.error() << std::endl;
return 1;
}
std::cout << "Setting input..." << std::endl;
auto input_result = vm.set_input(input_json);
if (!input_result) {
std::cerr << "set input: " << input_result.error() << std::endl;
return 1;
}
std::cout << "Executing entry point..." << std::endl;
auto exec_result = vm.execute();
if (!exec_result) {
std::cerr << "execute: " << exec_result.error() << std::endl;
return 1;
}
std::cout << "Execution result (data.demo.allow): " << exec_result.output() << std::endl;
std::cout << "Decision: user=alice action=read -> allow=" << exec_result.output() << std::endl;
if (std::string(exec_result.output()) != "true") {
std::cerr << "unexpected result: " << exec_result.output() << std::endl;
return 1;
}
}
regorus::Engine engine;
std::cout << "Compiling program from engine..." << std::endl;
auto add_policy_result = engine.add_policy("demo.rego", module_text);
if (!add_policy_result) {
std::cerr << "engine add policy: " << add_policy_result.error() << std::endl;
return 1;
}
auto engine_program_result = regorus::Program::compile_from_engine(
engine.raw(),
entry_points,
1
);
if (!engine_program_result) {
std::cerr << "compile program (engine): " << engine_program_result.error() << std::endl;
return 1;
}
regorus::Program engine_program = engine_program_result.program();
regorus::Rvm engine_vm;
auto engine_load_result = engine_vm.load_program(engine_program);
if (!engine_load_result) {
std::cerr << "engine load program: " << engine_load_result.error() << std::endl;
return 1;
}
std::cout << "Setting engine data..." << std::endl;
auto engine_data_result = engine_vm.set_data(data_json);
if (!engine_data_result) {
std::cerr << "engine set data: " << engine_data_result.error() << std::endl;
return 1;
}
std::cout << "Setting engine input..." << std::endl;
auto engine_input_result = engine_vm.set_input(input_json);
if (!engine_input_result) {
std::cerr << "engine set input: " << engine_input_result.error() << std::endl;
return 1;
}
std::cout << "Executing engine entry point..." << std::endl;
auto engine_exec_result = engine_vm.execute();
if (!engine_exec_result) {
std::cerr << "engine execute: " << engine_exec_result.error() << std::endl;
return 1;
}
std::cout << "Engine execution result (data.demo.allow): " << engine_exec_result.output() << std::endl;
std::cout << "Decision: user=alice action=read -> allow=" << engine_exec_result.output() << std::endl;
if (std::string(engine_exec_result.output()) != "true") {
std::cerr << "unexpected engine result: " << engine_exec_result.output() << std::endl;
return 1;
}
std::cout << "\n--- HostAwait example (suspendable execution) ---" << std::endl;
RegorusPolicyModule host_module;
host_module.id = "host_await.rego";
host_module.content = host_module_text;
const char* host_entry_points[] = {"data.demo.allow"};
auto host_program_result = regorus::Program::compile_from_modules(
host_data_json,
&host_module,
1,
host_entry_points,
1
);
if (!host_program_result) {
std::cerr << "compile host await program: " << host_program_result.error() << std::endl;
return 1;
}
regorus::Program host_program = host_program_result.program();
regorus::Rvm host_vm;
auto host_mode_result = host_vm.set_execution_mode(1);
if (!host_mode_result) {
std::cerr << "set execution mode: " << host_mode_result.error() << std::endl;
return 1;
}
auto host_load_result = host_vm.load_program(host_program);
if (!host_load_result) {
std::cerr << "load host await program: " << host_load_result.error() << std::endl;
return 1;
}
auto host_data_result = host_vm.set_data(host_data_json);
if (!host_data_result) {
std::cerr << "set host data: " << host_data_result.error() << std::endl;
return 1;
}
auto host_input_result = host_vm.set_input(host_input_json);
if (!host_input_result) {
std::cerr << "set host input: " << host_input_result.error() << std::endl;
return 1;
}
auto host_exec_result = host_vm.execute();
if (!host_exec_result) {
std::cerr << "execute host await: " << host_exec_result.error() << std::endl;
return 1;
}
std::cout << "HostAwait initial result: " << host_exec_result.output() << std::endl;
auto host_state_result = host_vm.get_execution_state();
if (!host_state_result) {
std::cerr << "get execution state: " << host_state_result.error() << std::endl;
return 1;
}
std::cout << "Execution state: " << host_state_result.output() << std::endl;
auto host_resume_result = host_vm.resume("{\"tier\":\"gold\"}", true);
if (!host_resume_result) {
std::cerr << "resume host await: " << host_resume_result.error() << std::endl;
return 1;
}
std::cout << "HostAwait resumed result: " << host_resume_result.output() << std::endl;
if (std::string(host_resume_result.output()) != "true") {
std::cerr << "unexpected host await result: " << host_resume_result.output() << std::endl;
return 1;
}
return 0;
}

View File

@@ -64,3 +64,43 @@ Regorus.MemoryLimits.SetThreadFlushThresholdOverride(null);
```
See bindings/csharp/Regorus.Tests/RegorusTests.cs for scenario coverage and bindings/csharp/TargetExampleApp/Program.cs for end-to-end usage.
## RVM Usage Example
The RVM API lets you compile a program from modules/entrypoints and execute it in a VM:
```csharp
using Regorus;
const string Policy = """
package demo
default allow = false
allow if {
input.user == "alice"
some role in data.roles[input.user]
role == "admin"
}
""";
const string Data = """
{ "roles": { "alice": ["admin"] } }
""";
const string Input = """
{ "user": "alice" }
""";
var modules = new[] { new PolicyModule("demo.rego", Policy) };
var entryPoints = new[] { "data.demo.allow" };
using var program = Program.CompileFromModules(Data, modules, entryPoints);
var listing = program.GenerateListing();
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetDataJson(Data);
vm.SetInputJson(Input);
var result = vm.Execute();
Console.WriteLine($"allow: {result}");
```

View File

@@ -0,0 +1,119 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Regorus.Tests;
[TestClass]
public sealed class RvmProgramTests
{
private const string Policy = """
package demo
default allow = false
allow if {
input.user == "alice"
some role in data.roles[input.user]
role == "admin"
count(input.actions) > 0
}
""";
private const string Data = """
{
"roles": {
"alice": ["admin", "reader"]
}
}
""";
private const string Input = """
{
"user": "alice",
"actions": ["read"]
}
""";
private const string HostAwaitPolicy = """
package demo
import rego.v1
default allow := false
allow if {
input.account.active == true
details := __builtin_host_await(input.account.id, "account")
details.tier == "gold"
}
""";
private const string HostAwaitInput = """
{
"account": {
"id": "acct-1",
"active": true
}
}
""";
[TestMethod]
public void Program_compile_and_execute_succeeds()
{
var modules = new[] { new PolicyModule("demo.rego", Policy) };
var entryPoints = new[] { "data.demo.allow" };
var program = Program.CompileFromModules(Data, modules, entryPoints);
var listing = program.GenerateListing();
Assert.IsFalse(string.IsNullOrWhiteSpace(listing), "listing should be generated");
var binary = program.SerializeBinary();
var rehydrated = Program.DeserializeBinary(binary, out var isPartial);
Assert.IsFalse(isPartial, "program should be fully deserialized");
using var vm = new Rvm();
vm.LoadProgram(rehydrated);
vm.SetDataJson(Data);
vm.SetInputJson(Input);
var result = vm.Execute();
Assert.AreEqual("true", result, "expected allow=true");
}
[TestMethod]
public void Program_compile_from_engine_succeeds()
{
using var engine = new Engine();
engine.AddPolicy("demo.rego", Policy);
var program = Program.CompileFromEngine(engine, new[] { "data.demo.allow" });
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetDataJson(Data);
vm.SetInputJson(Input);
var result = vm.Execute();
Assert.AreEqual("true", result, "expected allow=true");
}
[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" };
using var program = Program.CompileFromModules("{}", modules, entryPoints);
using var vm = new Rvm();
vm.SetExecutionMode(1);
vm.LoadProgram(program);
vm.SetInputJson(HostAwaitInput);
var initial = vm.Execute();
var state = vm.GetExecutionState();
Assert.IsNotNull(state, "execution state should be available");
StringAssert.Contains(state!, "HostAwait", "expected HostAwait suspension");
var resumed = vm.Resume("{\"tier\":\"gold\"}");
Assert.AreEqual("true", resumed, "expected allow=true after resume");
}
}

View File

@@ -180,7 +180,7 @@ namespace Regorus
return handle;
}
private T UseHandle<T>(Func<IntPtr, T> func)
internal T UseHandle<T>(Func<IntPtr, T> func)
{
var handle = GetHandleForUse();
bool addedRef = false;
@@ -204,6 +204,11 @@ namespace Regorus
}
}
internal T UseHandleForInterop<T>(Func<IntPtr, T> func)
{
return UseHandle(func);
}
private void UseHandle(Action<IntPtr> action)
{
UseHandle<object?>(handlePtr =>

View File

@@ -441,7 +441,7 @@ namespace Regorus
}
}
private RegorusEngineHandle GetHandleForUse()
internal RegorusEngineHandle GetHandleForUse()
{
var handle = _handle;
if (handle is null || handle.IsClosed || handle.IsInvalid)
@@ -451,7 +451,7 @@ namespace Regorus
return handle;
}
private void UseHandle(Action<IntPtr> action)
internal void UseHandle(Action<IntPtr> action)
{
UseHandle<object?>(handlePtr =>
{
@@ -460,7 +460,7 @@ namespace Regorus
});
}
private T UseHandle<T>(Func<IntPtr, T> func)
internal T UseHandle<T>(Func<IntPtr, T> func)
{
var handle = GetHandleForUse();
bool addedRef = false;
@@ -484,5 +484,10 @@ namespace Regorus
}
}
internal T UseHandleForInterop<T>(Func<IntPtr, T> func)
{
return UseHandle(func);
}
}
}

View File

@@ -26,6 +26,13 @@ namespace Regorus.Internal
[DllImport(LibraryName, EntryPoint = "regorus_result_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_result_drop(RegorusResult result);
/// <summary>
/// Drop a RegorusBuffer.
/// data is not valid after drop.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_buffer_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_buffer_drop(RegorusBuffer* buffer);
#endregion
#region Memory Limit Methods
@@ -85,6 +92,12 @@ namespace Regorus.Internal
[DllImport(LibraryName, EntryPoint = "regorus_engine_clone", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusEngine* regorus_engine_clone(RegorusEngine* engine);
/// <summary>
/// Compile an RVM program from the engine state with entry points.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_compile_program_with_entrypoints", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_compile_program_with_entrypoints(RegorusEngine* engine, byte** entryPoints, UIntPtr entryPointsLen);
/// <summary>
/// Drop a RegorusEngine.
/// </summary>
@@ -92,6 +105,138 @@ namespace Regorus.Internal
internal static extern void regorus_engine_drop(RegorusEngine* engine);
/// <summary>
/// <summary>
/// Compile an RVM program from data/modules and entry points.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_program_compile_from_modules", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_program_compile_from_modules(byte* data_json, RegorusPolicyModule* modules, UIntPtr modules_len, byte** entry_points, UIntPtr entry_points_len);
/// <summary>
/// Construct a new empty program.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_program_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusProgram* regorus_program_new();
/// <summary>
/// Drop a program handle.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_program_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_program_drop(RegorusProgram* program);
/// <summary>
/// Serialize a program to binary format.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_program_serialize_binary", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_program_serialize_binary(RegorusProgram* program);
/// <summary>
/// Deserialize a program from binary format.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_program_deserialize_binary", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_program_deserialize_binary(byte* data, UIntPtr len, byte* is_partial);
/// <summary>
/// Generate a readable assembly listing for the program.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_program_generate_listing", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_program_generate_listing(RegorusProgram* program);
/// <summary>
/// Create a new RVM instance.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusRvm* regorus_rvm_new();
/// <summary>
/// Create a new RVM instance from a compiled policy.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_new_with_policy", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_new_with_policy(RegorusCompiledPolicy* compiled_policy);
/// <summary>
/// Drop an RVM instance.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_rvm_drop(RegorusRvm* vm);
/// <summary>
/// Load a program into the RVM.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_load_program", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_load_program(RegorusRvm* vm, RegorusProgram* program);
/// <summary>
/// Set the data document for the RVM.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_set_data", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_set_data(RegorusRvm* vm, byte* data_json);
/// <summary>
/// Set the input document for the RVM.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_set_input", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_set_input(RegorusRvm* vm, byte* input_json);
/// <summary>
/// Execute the program.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_execute", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_execute(RegorusRvm* vm);
/// <summary>
/// Execute an entry point by name.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_execute_entry_point_by_name", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_execute_entry_point_by_name(RegorusRvm* vm, byte* entry_point);
/// <summary>
/// Execute an entry point by index.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_execute_entry_point_by_index", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_execute_entry_point_by_index(RegorusRvm* vm, UIntPtr index);
/// <summary>
/// Resume execution.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_resume", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_resume(RegorusRvm* vm, byte* resume_value_json, [MarshalAs(UnmanagedType.I1)] bool has_value);
/// <summary>
/// Get the current execution state.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_get_execution_state", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_get_execution_state(RegorusRvm* vm);
/// <summary>
/// Set the maximum instruction limit.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_set_max_instructions", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_set_max_instructions(RegorusRvm* vm, UIntPtr max_instructions);
/// <summary>
/// Set strict builtin error handling.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_set_strict_builtin_errors", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_set_strict_builtin_errors(RegorusRvm* vm, [MarshalAs(UnmanagedType.I1)] bool strict);
/// <summary>
/// Set execution mode (0 run-to-completion, 1 suspendable).
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_set_execution_mode", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_set_execution_mode(RegorusRvm* vm, byte mode);
/// <summary>
/// Set step mode for suspendable execution.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_set_step_mode", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_set_step_mode(RegorusRvm* vm, [MarshalAs(UnmanagedType.I1)] bool enabled);
/// <summary>
/// Set execution timer configuration.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_set_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_set_execution_timer_config(RegorusRvm* vm, [MarshalAs(UnmanagedType.I1)] bool has_config, RegorusExecutionTimerConfig config);
/// Add a policy.
/// The policy is parsed into AST.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_policy
@@ -617,6 +762,17 @@ namespace Regorus.Internal
public uint check_interval;
}
/// <summary>
/// Byte buffer returned from FFI.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct RegorusBuffer
{
public byte* data;
public UIntPtr len;
public UIntPtr capacity;
}
/// <summary>
/// Wrapper for regorus::Engine.
/// </summary>
@@ -633,6 +789,22 @@ namespace Regorus.Internal
{
}
/// <summary>
/// Wrapper for regorus::rvm::Program.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusProgram
{
}
/// <summary>
/// Wrapper for regorus::rvm::RegoVM.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusRvm
{
}
/// <summary>
/// FFI wrapper for PolicyModule struct.
/// </summary>

View File

@@ -0,0 +1,333 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Regorus.Internal;
#nullable enable
namespace Regorus
{
/// <summary>
/// Represents a compiled RVM program.
/// </summary>
public unsafe sealed class Program : IDisposable
{
private RegorusProgramHandle? _handle;
private int _isDisposed;
private Program(RegorusProgramHandle handle)
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
/// <summary>
/// Create an empty program.
/// </summary>
public static Program CreateEmpty()
{
return new Program(RegorusProgramHandle.Create());
}
/// <summary>
/// Compile an RVM program from modules and entry points.
/// </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)
{
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];
try
{
for (int i = 0; i < modulesArray.Length; i++)
{
var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id);
var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content);
pinnedStrings.Add(idPinned);
pinnedStrings.Add(contentPinned);
nativeModules[i] = new RegorusPolicyModule
{
id = idPinned.Pointer,
content = contentPinned.Pointer
};
}
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>
/// Compile an RVM program from an engine instance and entry points.
/// </summary>
public static Program CompileFromEngine(Engine engine, IEnumerable<string> entryPoints)
{
if (engine is null)
{
throw new ArgumentNullException(nameof(engine));
}
var entryPointsArray = entryPoints.ToArray();
if (entryPointsArray.Length == 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;
}
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
{
foreach (var pinned in pinnedStrings)
{
pinned.Dispose();
}
}
}
/// <summary>
/// Deserialize an RVM program from binary format.
/// </summary>
public static Program DeserializeBinary(byte[] data, out bool isPartial)
{
if (data is null)
{
throw new ArgumentNullException(nameof(data));
}
byte partialFlag = 0;
fixed (byte* dataPtr = data)
{
var result = API.regorus_program_deserialize_binary(dataPtr, (UIntPtr)data.Length, &partialFlag);
var program = GetProgramResult(result);
isPartial = partialFlag != 0;
return program;
}
}
/// <summary>
/// Serialize the program to binary format.
/// </summary>
public byte[] SerializeBinary()
{
ThrowIfDisposed();
return UseHandle(programPtr =>
{
var result = API.regorus_program_serialize_binary((RegorusProgram*)programPtr);
return ExtractBuffer(result);
});
}
/// <summary>
/// Generate a readable assembly listing.
/// </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
{
if (result.status != RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
if (result.data_type != RegorusDataType.Pointer || result.pointer_value == null)
{
throw new Exception("Expected program pointer but got different data type");
}
var handle = RegorusProgramHandle.FromPointer((IntPtr)result.pointer_value);
return new Program(handle);
}
finally
{
API.regorus_result_drop(result);
}
}
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);
}
}
private static byte[] ExtractBuffer(RegorusResult result)
{
RegorusBuffer* buffer = null;
try
{
if (result.status != RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
if (result.data_type != RegorusDataType.Pointer || result.pointer_value == null)
{
throw new Exception("Expected buffer pointer but got different data type");
}
buffer = (RegorusBuffer*)result.pointer_value;
var length = checked((int)buffer->len);
var data = new byte[length];
if (length > 0)
{
Marshal.Copy((IntPtr)buffer->data, data, 0, length);
}
return data;
}
finally
{
if (buffer != null)
{
API.regorus_buffer_drop(buffer);
}
API.regorus_result_drop(result);
}
}
}
}

View File

@@ -0,0 +1,292 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using Regorus.Internal;
#nullable enable
namespace Regorus
{
/// <summary>
/// Wrapper for the Regorus RVM runtime.
/// </summary>
public unsafe sealed class Rvm : IDisposable
{
private RegorusRvmHandle? _handle;
private int _isDisposed;
public Rvm()
{
_handle = RegorusRvmHandle.Create();
}
private Rvm(RegorusRvmHandle handle)
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
/// <summary>
/// Create an RVM instance backed by a compiled policy (for default rule evaluation).
/// </summary>
public static Rvm CreateWithPolicy(CompiledPolicy policy)
{
if (policy is null)
{
throw new ArgumentNullException(nameof(policy));
}
return policy.UseHandleForInterop(policyPtr =>
{
var result = API.regorus_rvm_new_with_policy((RegorusCompiledPolicy*)policyPtr);
return GetRvmResult(result);
});
}
/// <summary>
/// Load a program into the VM.
/// </summary>
public void LoadProgram(Program program)
{
ThrowIfDisposed();
if (program is null)
{
throw new ArgumentNullException(nameof(program));
}
program.UseHandle(programPtr =>
{
UseHandle(vmPtr =>
{
CheckAndDropResult(API.regorus_rvm_load_program((RegorusRvm*)vmPtr, (RegorusProgram*)programPtr));
return 0;
});
return 0;
});
}
/// <summary>
/// Set the data document for the VM.
/// </summary>
public void SetDataJson(string dataJson)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
{
UseHandle(vmPtr =>
{
CheckAndDropResult(API.regorus_rvm_set_data((RegorusRvm*)vmPtr, (byte*)dataPtr));
return 0;
});
});
}
/// <summary>
/// Set the input document for the VM.
/// </summary>
public void SetInputJson(string inputJson)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(inputJson, inputPtr =>
{
UseHandle(vmPtr =>
{
CheckAndDropResult(API.regorus_rvm_set_input((RegorusRvm*)vmPtr, (byte*)inputPtr));
return 0;
});
});
}
/// <summary>
/// Set the execution mode (0 = run-to-completion, 1 = suspendable).
/// </summary>
public void SetExecutionMode(byte mode)
{
ThrowIfDisposed();
UseHandle(vmPtr =>
{
CheckAndDropResult(API.regorus_rvm_set_execution_mode((RegorusRvm*)vmPtr, mode));
return 0;
});
}
/// <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));
});
}
/// <summary>
/// Execute a named entry point.
/// </summary>
public string? ExecuteEntryPoint(string entryPoint)
{
ThrowIfDisposed();
return Utf8Marshaller.WithUtf8(entryPoint, entryPtr =>
{
return UseHandle(vmPtr =>
{
return CheckAndDropResult(API.regorus_rvm_execute_entry_point_by_name((RegorusRvm*)vmPtr, (byte*)entryPtr));
});
});
}
/// <summary>
/// Execute an entry point by index.
/// </summary>
public string? ExecuteEntryPoint(ulong index)
{
ThrowIfDisposed();
return UseHandle(vmPtr =>
{
return CheckAndDropResult(API.regorus_rvm_execute_entry_point_by_index((RegorusRvm*)vmPtr, (UIntPtr)index));
});
}
/// <summary>
/// Resume execution with an optional value.
/// </summary>
public string? Resume(string? resumeValueJson)
{
ThrowIfDisposed();
if (resumeValueJson is null)
{
return UseHandle(vmPtr =>
{
return CheckAndDropResult(API.regorus_rvm_resume((RegorusRvm*)vmPtr, null, has_value: false));
});
}
return Utf8Marshaller.WithUtf8(resumeValueJson, valuePtr =>
{
return UseHandle(vmPtr =>
{
return CheckAndDropResult(API.regorus_rvm_resume((RegorusRvm*)vmPtr, (byte*)valuePtr, has_value: true));
});
});
}
/// <summary>
/// Get the current execution state.
/// </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
{
if (result.status != RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
if (result.data_type != RegorusDataType.Pointer || result.pointer_value == null)
{
throw new Exception("Expected RVM pointer but got different data type");
}
var handle = RegorusRvmHandle.FromPointer((IntPtr)result.pointer_value);
return new Rvm(handle);
}
finally
{
API.regorus_result_drop(result);
}
}
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);
}
}
}
}

View File

@@ -87,4 +87,100 @@ namespace Regorus
return true;
}
}
internal sealed class RegorusProgramHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private RegorusProgramHandle() : base(ownsHandle: true)
{
}
internal static RegorusProgramHandle Create()
{
unsafe
{
var raw = Internal.API.regorus_program_new();
if (raw is null)
{
throw new InvalidOperationException("Failed to create Regorus program.");
}
var handle = new RegorusProgramHandle();
handle.SetHandle((IntPtr)raw);
return handle;
}
}
internal static RegorusProgramHandle FromPointer(IntPtr pointer)
{
if (pointer == IntPtr.Zero)
{
throw new ArgumentException("Pointer cannot be zero.", nameof(pointer));
}
var handle = new RegorusProgramHandle();
handle.SetHandle(pointer);
return handle;
}
protected override bool ReleaseHandle()
{
if (!IsInvalid && !IsClosed)
{
unsafe
{
Internal.API.regorus_program_drop((Internal.RegorusProgram*)handle);
}
SetHandle(IntPtr.Zero);
}
return true;
}
}
internal sealed class RegorusRvmHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private RegorusRvmHandle() : base(ownsHandle: true)
{
}
internal static RegorusRvmHandle Create()
{
unsafe
{
var raw = Internal.API.regorus_rvm_new();
if (raw is null)
{
throw new InvalidOperationException("Failed to create Regorus RVM.");
}
var handle = new RegorusRvmHandle();
handle.SetHandle((IntPtr)raw);
return handle;
}
}
internal static RegorusRvmHandle FromPointer(IntPtr pointer)
{
if (pointer == IntPtr.Zero)
{
throw new ArgumentException("Pointer cannot be zero.", nameof(pointer));
}
var handle = new RegorusRvmHandle();
handle.SetHandle(pointer);
return handle;
}
protected override bool ReleaseHandle()
{
if (!IsInvalid && !IsClosed)
{
unsafe
{
Internal.API.regorus_rvm_drop((Internal.RegorusRvm*)handle);
}
SetHandle(IntPtr.Zero);
}
return true;
}
}
}

View File

@@ -65,6 +65,55 @@ 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 = """
package demo
import rego.v1
default allow := false
allow if {
input.user == "alice"
some role in data.roles[input.user]
role == "admin"
}
""";
private const string RVM_DATA = """
{
"roles": {
"alice": ["admin", "reader"]
}
}
""";
private const string RVM_INPUT = """
{
"user": "alice"
}
""";
private const string HOST_AWAIT_POLICY = """
package demo
import rego.v1
default allow := false
allow if {
input.account.active == true
details := __builtin_host_await(input.account.id, "account")
details.tier == "gold"
}
""";
private const string HOST_AWAIT_INPUT = """
{
"account": {
"id": "acct-1",
"active": true
}
}
""";
// Test data constants
private const string COMPLIANT_STORAGE_ACCOUNT = @"{
""type"": ""Microsoft.Storage/storageAccounts"",
@@ -174,6 +223,15 @@ triplet_count := count([1 |
Console.WriteLine("\n5. Execution timer configuration:");
DemonstrateExecutionTimer();
Console.WriteLine("\n6. RVM program execution:");
DemonstrateRvmUsage();
Console.WriteLine("\n7. RVM program compilation from engine:");
DemonstrateRvmCompileFromEngine();
Console.WriteLine("\n8. RVM host await (suspend/resume):");
DemonstrateRvmHostAwait();
}
static void DemonstrateConcurrentEvaluation(Regorus.CompiledPolicy compiledPolicy)
@@ -359,4 +417,77 @@ triplet_count := count([1 |
Regorus.Engine.ClearFallbackExecutionTimerConfig();
}
}
static void DemonstrateRvmUsage()
{
var modules = new List<Regorus.PolicyModule>
{
new Regorus.PolicyModule("demo.rego", RVM_POLICY)
};
var entryPoints = new[] { "data.demo.allow" };
using var program = Regorus.Program.CompileFromModules(RVM_DATA, modules, entryPoints);
var binary = program.SerializeBinary();
using var rehydrated = Regorus.Program.DeserializeBinary(binary, out var isPartial);
if (isPartial)
{
throw new InvalidOperationException("RVM program deserialization returned a partial program.");
}
Console.WriteLine($"Serialized program size: {binary.Length} bytes");
var listing = rehydrated.GenerateListing();
Console.WriteLine("RVM listing:");
Console.WriteLine(listing);
using var vm = new Regorus.Rvm();
vm.LoadProgram(rehydrated);
vm.SetDataJson(RVM_DATA);
vm.SetInputJson(RVM_INPUT);
var result = vm.Execute();
Console.WriteLine($"RVM result: {result}");
}
static void DemonstrateRvmCompileFromEngine()
{
using var engine = new Regorus.Engine();
engine.AddPolicy("demo.rego", RVM_POLICY);
engine.AddDataJson(RVM_DATA);
var entryPoints = new[] { "data.demo.allow" };
using var program = Regorus.Program.CompileFromEngine(engine, entryPoints);
using var vm = new Regorus.Rvm();
vm.LoadProgram(program);
vm.SetDataJson(RVM_DATA);
vm.SetInputJson(RVM_INPUT);
var result = vm.ExecuteEntryPoint("data.demo.allow");
Console.WriteLine($"RVM result from engine-compiled program: {result}");
}
static void DemonstrateRvmHostAwait()
{
var modules = new List<Regorus.PolicyModule>
{
new Regorus.PolicyModule("host_await.rego", HOST_AWAIT_POLICY)
};
var entryPoints = new[] { "data.demo.allow" };
using var program = Regorus.Program.CompileFromModules("{}", modules, entryPoints);
using var vm = new Regorus.Rvm();
vm.SetExecutionMode(1);
vm.LoadProgram(program);
vm.SetInputJson(HOST_AWAIT_INPUT);
var initial = vm.Execute();
var state = vm.GetExecutionState();
Console.WriteLine($"HostAwait initial result: {initial}");
Console.WriteLine($"Execution state: {state}");
var resumed = vm.Resume("{\"tier\":\"gold\"}");
Console.WriteLine($"HostAwait resumed result: {resumed}");
}
}

View File

@@ -102,6 +102,16 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"serde",
"unty",
]
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -172,9 +182,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.54"
version = "1.2.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -211,18 +221,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.54"
version = "4.5.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.54"
version = "4.5.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0"
dependencies = [
"anstream",
"anstyle",
@@ -329,9 +339,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fluent-uri"
@@ -407,9 +417,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.64"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -539,6 +549,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
[[package]]
@@ -941,11 +953,13 @@ name = "regorus"
version = "0.9.0"
dependencies = [
"anyhow",
"bincode",
"chrono",
"chrono-tz",
"dashmap",
"data-encoding",
"globset",
"indexmap",
"ipnet",
"jsonschema",
"lazy_static",
@@ -1244,6 +1258,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "url"
version = "2.5.8"
@@ -1468,18 +1488,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.34"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.34"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -30,6 +30,7 @@ default = [
"std",
"coverage",
"allocator-memory-limits",
"rvm",
"regorus/arc",
"regorus/full-opa",
"contention_checks",
@@ -40,6 +41,7 @@ std = ["regorus/std"]
coverage = ["regorus/coverage"]
allocator-memory-limits = ["regorus/allocator-memory-limits"]
contention_checks = ["parking_lot"]
rvm = ["regorus/rvm"]
custom_allocator = []
[build-dependencies]

View File

@@ -1,12 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use alloc::boxed::Box;
use alloc::ffi::CString;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{anyhow, bail, Result};
use core::ffi::{c_char, c_longlong, c_void, CStr};
use core::ptr;
use core::{mem, ptr};
/// Status of a call on `RegorusEngine`.
#[repr(C)]
@@ -90,6 +92,19 @@ pub struct RegorusResult {
pub(crate) error_message: *mut c_char,
}
/// Byte buffer returned from FFI for binary payloads.
///
/// Must be freed using `regorus_buffer_drop`.
#[repr(C)]
pub struct RegorusBuffer {
/// Pointer to byte buffer data.
pub data: *mut u8,
/// Number of bytes stored in `data`.
pub len: usize,
/// Capacity of the allocation backing `data`.
pub capacity: usize,
}
impl RegorusResult {
/// Create a successful result with no data.
pub(crate) fn ok_void() -> Self {
@@ -185,6 +200,18 @@ impl RegorusResult {
}
}
impl RegorusBuffer {
pub(crate) fn from_vec(mut data: Vec<u8>) -> *mut RegorusBuffer {
let buffer = RegorusBuffer {
data: data.as_mut_ptr(),
len: data.len(),
capacity: data.capacity(),
};
mem::forget(data);
Box::into_raw(Box::new(buffer))
}
}
pub(crate) fn to_c_str(s: String) -> *mut c_char {
match CString::new(s) {
Ok(cs) => cs.into_raw(),
@@ -222,6 +249,21 @@ pub(crate) fn to_regorus_string_result(r: Result<String>) -> RegorusResult {
}
}
/// Drop a `RegorusBuffer`.
///
/// `data` is not valid after drop.
#[no_mangle]
pub extern "C" fn regorus_buffer_drop(buffer: *mut RegorusBuffer) {
if let Ok(buffer) = to_ref(buffer) {
unsafe {
if !buffer.data.is_null() {
let _ = Vec::from_raw_parts(buffer.data, buffer.len, buffer.capacity);
}
let _ = Box::from_raw(ptr::from_mut(buffer));
}
}
}
/// Drop a `RegorusResult`.
///
/// `output` and `error_message` strings are not valid after drop.

View File

@@ -11,9 +11,16 @@ use crate::panic_guard::with_unwind_guard;
use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
#[cfg(feature = "rvm")]
use alloc::sync::Arc;
use alloc::vec::Vec;
use anyhow::{anyhow, Result};
use core::ffi::{c_char, c_void};
use core::ptr;
#[cfg(feature = "rvm")]
use regorus::languages::rego::compiler::Compiler;
#[cfg(feature = "rvm")]
use regorus::rvm::program::Program;
/// Wrapper for `regorus::Engine`.
pub struct RegorusEngine {
@@ -720,3 +727,65 @@ pub extern "C" fn regorus_engine_compile_with_entrypoint(
}
})
}
/// Compile an RVM program from the engine state with entry points.
///
/// * `entry_points` - Array of entry point rule paths
/// * `entry_points_len` - Number of entry points
#[cfg(feature = "rvm")]
#[no_mangle]
pub extern "C" fn regorus_engine_compile_program_with_entrypoints(
engine: *mut RegorusEngine,
entry_points: *const *const c_char,
entry_points_len: usize,
) -> RegorusResult {
with_unwind_guard(|| {
let result = || -> Result<Arc<Program>> {
if entry_points_len == 0 {
return Err(anyhow!("entry_points must contain at least one entry"));
}
if entry_points.is_null() && entry_points_len > 0 {
return Err(anyhow!("null entry_points pointer"));
}
let mut entry_points_vec = Vec::with_capacity(entry_points_len);
for i in 0..entry_points_len {
unsafe {
let entry_ptr = entry_points.add(i);
if entry_ptr.is_null() {
return Err(anyhow!("null entry point at index {i}"));
}
let entry = from_c_str(*entry_ptr)?;
entry_points_vec.push(entry);
}
}
let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect();
let rule = entry_points_ref
.first()
.ok_or_else(|| anyhow!("entry_points must contain at least one entry"))?;
let rule_rc: regorus::Rc<str> = (*rule).into();
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?;
let program = Compiler::compile_from_policy(&compiled_policy, &entry_points_ref)?;
Ok(program)
}();
match result {
Ok(program) => {
let wrapped = crate::rvm::RegorusProgram { program };
let boxed = Box::new(wrapped);
RegorusResult::ok_pointer(Box::into_raw(boxed) as *mut c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Failed to compile RVM program: {e}"),
),
}
})
}

View File

@@ -14,5 +14,7 @@ mod engine;
mod limits;
mod lock;
mod panic_guard;
#[cfg(feature = "rvm")]
pub(crate) mod rvm;
mod schema_registry;
mod target_registry;

606
bindings/ffi/src/rvm.rs Normal file
View File

@@ -0,0 +1,606 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::common::{
from_c_str, to_ref, to_regorus_result, RegorusBuffer, RegorusResult, RegorusStatus,
};
use crate::compile::RegorusPolicyModule;
use crate::compiled_policy::RegorusCompiledPolicy;
use crate::limits::RegorusExecutionTimerConfig;
use crate::lock::{new_handle, try_read, try_write, Handle, ReadGuard, WriteGuard};
use crate::panic_guard::with_unwind_guard;
use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use anyhow::{anyhow, Result};
use core::ffi::{c_char, c_void};
use core::ptr;
use regorus::languages::rego::compiler::Compiler;
use regorus::rvm::program::{
generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig,
DeserializationResult, Program,
};
use regorus::rvm::vm::{ExecutionMode, ExecutionState, RegoVM};
use regorus::PolicyModule;
use regorus::Value;
/// Wrapper for `regorus::rvm::Program`.
#[derive(Clone)]
pub struct RegorusProgram {
pub(crate) program: Arc<Program>,
}
/// Wrapper for `regorus::rvm::RegoVM`.
pub struct RegorusRvm {
vm: Handle<RegoVM>,
}
impl RegorusRvm {
fn new(vm: RegoVM) -> Self {
Self { vm: new_handle(vm) }
}
fn contention_error() -> anyhow::Error {
anyhow!("regorus rvm handle is already in use; create a separate VM per thread")
}
fn try_write(&self) -> Result<WriteGuard<'_, RegoVM>> {
try_write(&self.vm).ok_or_else(Self::contention_error)
}
fn try_read(&self) -> Result<ReadGuard<'_, RegoVM>> {
try_read(&self.vm).ok_or_else(Self::contention_error)
}
}
/// Drop a `RegorusProgram`.
#[no_mangle]
pub extern "C" fn regorus_program_drop(program: *mut RegorusProgram) {
if let Ok(program) = to_ref(program) {
unsafe {
let _ = Box::from_raw(ptr::from_mut(program));
}
}
}
/// Drop a `RegorusRvm`.
#[no_mangle]
pub extern "C" fn regorus_rvm_drop(vm: *mut RegorusRvm) {
if let Ok(vm) = to_ref(vm) {
unsafe {
let _ = Box::from_raw(ptr::from_mut(vm));
}
}
}
/// Compile a compiled policy into an RVM program.
///
/// * `compiled_policy` - Compiled policy handle
/// * `entry_points` - Array of entry point rule paths
/// * `entry_points_len` - Number of entry points
#[no_mangle]
pub extern "C" fn regorus_program_compile_from_policy(
compiled_policy: *mut RegorusCompiledPolicy,
entry_points: *const *const c_char,
entry_points_len: usize,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<*mut RegorusProgram> {
if entry_points.is_null() && entry_points_len > 0 {
return Err(anyhow!("null entry_points pointer"));
}
let mut entry_points_vec = Vec::with_capacity(entry_points_len);
for i in 0..entry_points_len {
unsafe {
let entry_ptr = entry_points.add(i);
if entry_ptr.is_null() {
return Err(anyhow!("null entry point at index {i}"));
}
let entry = from_c_str(*entry_ptr)?;
entry_points_vec.push(entry);
}
}
let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect();
let compiled_policy = &to_ref(compiled_policy)?.compiled_policy;
let program = Compiler::compile_from_policy(compiled_policy, &entry_points_ref)?;
Ok(Box::into_raw(Box::new(RegorusProgram { program })))
}();
match output {
Ok(program) => RegorusResult::ok_pointer(program as *mut c_void),
Err(err) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("RVM compilation failed: {err}"),
),
}
})
}
/// Compile an RVM program from data/modules and entry points.
///
/// * `data_json` - JSON string containing static data for policy evaluation
/// * `modules` - Array of policy modules to compile
/// * `modules_len` - Number of modules in the array
/// * `entry_points` - Array of entry point rule paths
/// * `entry_points_len` - Number of entry points
#[no_mangle]
pub extern "C" fn regorus_program_compile_from_modules(
data_json: *const c_char,
modules: *const RegorusPolicyModule,
modules_len: usize,
entry_points: *const *const c_char,
entry_points_len: usize,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<*mut RegorusProgram> {
if entry_points_len == 0 {
return Err(anyhow!("entry_points must contain at least one entry"));
}
let data_str = from_c_str(data_json)?;
let data = Value::from_json_str(&data_str)?;
let policy_modules = convert_c_modules_to_rust(modules, modules_len)?;
let entry_points_vec = convert_c_entry_points(entry_points, entry_points_len)?;
let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect();
let entry_rule = entry_points_ref
.first()
.ok_or_else(|| anyhow!("entry_points must contain at least one entry"))?;
let compiled_policy = regorus::compile_policy_with_entrypoint(
data,
&policy_modules,
(*entry_rule).into(),
)?;
let program = Compiler::compile_from_policy(&compiled_policy, &entry_points_ref)?;
Ok(Box::into_raw(Box::new(RegorusProgram { program })))
}();
match output {
Ok(program) => RegorusResult::ok_pointer(program as *mut c_void),
Err(err) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("RVM compilation failed: {err}"),
),
}
})
}
/// Create a new, empty RVM program.
#[no_mangle]
pub extern "C" fn regorus_program_new() -> *mut RegorusProgram {
let program = Program::new();
Box::into_raw(Box::new(RegorusProgram {
program: Arc::new(program),
}))
}
/// Serialize a program to the binary RVM format.
#[no_mangle]
pub extern "C" fn regorus_program_serialize_binary(program: *mut RegorusProgram) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<*mut RegorusBuffer> {
let program = &to_ref(program)?.program;
let bytes = program.serialize_binary().map_err(|e| anyhow!(e))?;
Ok(RegorusBuffer::from_vec(bytes))
}();
match output {
Ok(buffer) => RegorusResult::ok_pointer(buffer as *mut c_void),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{err}")),
}
})
}
/// Deserialize a program from the binary RVM format.
///
/// Returns a `RegorusProgram` handle and sets `is_partial` to true when the
/// program requires recompilation.
#[no_mangle]
pub extern "C" fn regorus_program_deserialize_binary(
data: *const u8,
len: usize,
is_partial: *mut bool,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<(*mut RegorusProgram, bool)> {
if data.is_null() && len > 0 {
return Err(anyhow!("null data pointer"));
}
let data = unsafe { core::slice::from_raw_parts(data, len) };
let (program, partial) =
match Program::deserialize_binary(data).map_err(|e| anyhow!(e))? {
DeserializationResult::Complete(program) => (program, false),
DeserializationResult::Partial(program) => (program, true),
};
Ok((
Box::into_raw(Box::new(RegorusProgram {
program: Arc::new(program),
})),
partial,
))
}();
match output {
Ok((program, partial)) => {
if !is_partial.is_null() {
unsafe {
*is_partial = partial;
}
}
RegorusResult::ok_pointer(program as *mut c_void)
}
Err(err) => {
RegorusResult::err_with_message(RegorusStatus::InvalidDataFormat, err.to_string())
}
}
})
}
/// Generate a default assembly listing for the program.
#[no_mangle]
pub extern "C" fn regorus_program_generate_listing(program: *mut RegorusProgram) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let program = &to_ref(program)?.program;
Ok(generate_assembly_listing(
program,
&AssemblyListingConfig::default(),
))
}();
match output {
Ok(listing) => RegorusResult::ok_string(listing),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{err}")),
}
})
}
/// Generate a tabular assembly listing for the program.
#[no_mangle]
pub extern "C" fn regorus_program_generate_tabular_listing(
program: *mut RegorusProgram,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let program = &to_ref(program)?.program;
Ok(generate_tabular_assembly_listing(
program,
&AssemblyListingConfig::default(),
))
}();
match output {
Ok(listing) => RegorusResult::ok_string(listing),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{err}")),
}
})
}
/// Construct a new RVM instance.
#[no_mangle]
pub extern "C" fn regorus_rvm_new() -> *mut RegorusRvm {
Box::into_raw(Box::new(RegorusRvm::new(RegoVM::new())))
}
/// Construct a new RVM instance with a compiled policy for default rule evaluation.
#[no_mangle]
pub extern "C" fn regorus_rvm_new_with_policy(
compiled_policy: *mut RegorusCompiledPolicy,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<*mut RegorusRvm> {
let policy = to_ref(compiled_policy)?.compiled_policy.clone();
Ok(Box::into_raw(Box::new(RegorusRvm::new(
RegoVM::new_with_policy(policy),
))))
}();
match output {
Ok(vm) => RegorusResult::ok_pointer(vm as *mut c_void),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
/// Load a program into the RVM.
#[no_mangle]
pub extern "C" fn regorus_rvm_load_program(
vm: *mut RegorusRvm,
program: *mut RegorusProgram,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
let program = to_ref(program)?.program.clone();
guard.load_program(program);
Ok(())
}())
})
}
/// Set the VM data document from JSON.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_data(vm: *mut RegorusRvm, data: *const c_char) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
let data_value = Value::from_json_str(&from_c_str(data)?)?;
guard.set_data(data_value)?;
Ok(())
}())
})
}
/// Set the VM input document from JSON.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_input(
vm: *mut RegorusRvm,
input: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
let input_value = Value::from_json_str(&from_c_str(input)?)?;
guard.set_input(input_value);
Ok(())
}())
})
}
/// Set the maximum number of instructions that can execute.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_max_instructions(
vm: *mut RegorusRvm,
max_instructions: usize,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
guard.set_max_instructions(max_instructions);
Ok(())
}())
})
}
/// Configure strict builtin error behavior.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_strict_builtin_errors(
vm: *mut RegorusRvm,
strict: bool,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
guard.set_strict_builtin_errors(strict);
Ok(())
}())
})
}
/// Configure the execution mode (0 = run-to-completion, 1 = suspendable).
#[no_mangle]
pub extern "C" fn regorus_rvm_set_execution_mode(vm: *mut RegorusRvm, mode: u8) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
let mode = match mode {
0 => ExecutionMode::RunToCompletion,
1 => ExecutionMode::Suspendable,
_ => return Err(anyhow!("invalid execution mode: {mode}")),
};
guard.set_execution_mode(mode);
Ok(())
}())
})
}
/// Enable or disable step mode when running suspendable execution.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_step_mode(vm: *mut RegorusRvm, enabled: bool) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
guard.set_step_mode(enabled);
Ok(())
}())
})
}
/// Configure the per-VM execution timer override.
#[no_mangle]
pub extern "C" fn regorus_rvm_set_execution_timer_config(
vm: *mut RegorusRvm,
has_config: bool,
config: RegorusExecutionTimerConfig,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
if has_config {
guard.set_execution_timer_config(Some(config.to_execution_timer_config()?));
} else {
guard.set_execution_timer_config(None);
}
Ok(())
}())
})
}
/// Execute the program's main entry point.
#[no_mangle]
pub extern "C" fn regorus_rvm_execute(vm: *mut RegorusRvm) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
let result = guard.execute()?;
result.to_json_str()
}();
match output {
Ok(json) => RegorusResult::ok_string(json),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
/// Execute a named entry point.
#[no_mangle]
pub extern "C" fn regorus_rvm_execute_entry_point_by_name(
vm: *mut RegorusRvm,
entry_point: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
let name = from_c_str(entry_point)?;
let result = guard.execute_entry_point_by_name(&name)?;
result.to_json_str()
}();
match output {
Ok(json) => RegorusResult::ok_string(json),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
/// Execute an entry point by index.
#[no_mangle]
pub extern "C" fn regorus_rvm_execute_entry_point_by_index(
vm: *mut RegorusRvm,
index: usize,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
let result = guard.execute_entry_point_by_index(index)?;
result.to_json_str()
}();
match output {
Ok(json) => RegorusResult::ok_string(json),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
/// Resume execution for suspendable runs.
#[no_mangle]
pub extern "C" fn regorus_rvm_resume(
vm: *mut RegorusRvm,
resume_value_json: *const c_char,
has_value: bool,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let vm = to_ref(vm)?;
let mut guard = vm.try_write()?;
let value = if has_value {
Some(Value::from_json_str(&from_c_str(resume_value_json)?)?)
} else {
None
};
let result = guard.resume(value)?;
result.to_json_str()
}();
match output {
Ok(json) => RegorusResult::ok_string(json),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
/// Get the current execution state of the VM.
#[no_mangle]
pub extern "C" fn regorus_rvm_get_execution_state(vm: *mut RegorusRvm) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let vm = to_ref(vm)?;
let guard = vm.try_read()?;
let state: ExecutionState = guard.execution_state().clone();
Ok(format!("{:?}", state))
}();
match output {
Ok(json) => RegorusResult::ok_string(json),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()),
}
})
}
fn convert_c_entry_points(
entry_points: *const *const c_char,
entry_points_len: usize,
) -> Result<Vec<String>> {
if entry_points.is_null() && entry_points_len > 0 {
return Err(anyhow!("null entry_points pointer"));
}
let mut entry_points_vec = Vec::with_capacity(entry_points_len);
for i in 0..entry_points_len {
unsafe {
let entry_ptr = entry_points.add(i);
if entry_ptr.is_null() {
return Err(anyhow!("null entry point at index {i}"));
}
let entry = from_c_str(*entry_ptr)?;
entry_points_vec.push(entry);
}
}
Ok(entry_points_vec)
}
fn convert_c_modules_to_rust(
modules: *const RegorusPolicyModule,
modules_len: usize,
) -> Result<Vec<PolicyModule>> {
if modules.is_null() && modules_len > 0 {
return Err(anyhow!("null modules pointer"));
}
let mut policy_modules = Vec::with_capacity(modules_len);
for i in 0..modules_len {
unsafe {
let module = modules.add(i);
if module.is_null() {
return Err(anyhow!("null module at index {i}"));
}
let module_ref = &*module;
let id = from_c_str(module_ref.id)
.map_err(|e| anyhow!("invalid module id at index {i}: {e}"))?;
let content = from_c_str(module_ref.content)
.map_err(|e| anyhow!("invalid module content at index {i}: {e}"))?;
policy_modules.push(PolicyModule {
id: id.into(),
content: content.into(),
});
}
}
Ok(policy_modules)
}

View File

@@ -100,4 +100,138 @@ func main() {
os.Exit(1)
}
fmt.Printf("%s\n", output)
// RVM regular example (compile, serialize, execute)
const regularPolicy = `
package demo
import rego.v1
default allow := false
allow if {
input.user == "alice"
input.active == true
}
`
const regularInput = `{"user":"alice","active":true}`
regularModules := []regorus.PolicyModule{{Id: "demo.rego", Content: regularPolicy}}
regularEntryPoints := []string{"data.demo.allow"}
regularProgram, err := regorus.CompileProgramFromModules("{}", regularModules, regularEntryPoints)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
defer regularProgram.Close()
listing, err := regularProgram.GenerateListing()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Printf("RVM listing:\n%s\n", listing)
binary, err := regularProgram.SerializeBinary()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
regularProgram.Close()
rehydrated, isPartial, err := regorus.DeserializeProgram(binary)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if isPartial {
fmt.Fprintf(os.Stderr, "error: program marked partial\n")
os.Exit(1)
}
defer rehydrated.Close()
regularVm, err := regorus.NewRvm()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
defer regularVm.Close()
if err := regularVm.LoadProgram(rehydrated); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if err := regularVm.SetInputJson(regularInput); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
regularResult, err := regularVm.Execute()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Printf("RVM regular result: %s\n", regularResult)
// RVM HostAwait example
const rvmPolicy = `
package demo
import rego.v1
default allow := false
allow if {
input.account.active == true
details := __builtin_host_await(input.account.id, "account")
details.tier == "gold"
}
`
const rvmInput = `{"account":{"id":"acct-1","active":true}}`
modules := []regorus.PolicyModule{{Id: "demo.rego", Content: rvmPolicy}}
entryPoints := []string{"data.demo.allow"}
program, err := regorus.CompileProgramFromModules("{}", modules, entryPoints)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
defer program.Close()
vm, err := regorus.NewRvm()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
defer vm.Close()
if err := vm.SetExecutionMode(1); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if err := vm.LoadProgram(program); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if err := vm.SetInputJson(rvmInput); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if _, err := vm.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
state, err := vm.GetExecutionState()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Printf("HostAwait state: %s\n", state)
result, err := vm.Resume(`{"tier":"gold"}`, true)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Printf("HostAwait result: %s\n", result)
}

View File

@@ -1,6 +1,6 @@
package regorus
// #cgo LDFLAGS: -L ../../../ffi/target/release -lregorus_ffi
// #cgo LDFLAGS: -L ../../../ffi/target/release -L ../../../ffi/target/debug -lregorus_ffi
// #include "../../../ffi/regorus.h"
import "C"
import (

View File

@@ -0,0 +1,283 @@
package regorus
// #cgo LDFLAGS: -L ../../../ffi/target/release -L ../../../ffi/target/debug -lregorus_ffi
// #include "../../../ffi/regorus.h"
import "C"
import (
"fmt"
"unsafe"
)
type PolicyModule struct {
Id string
Content string
}
type Program struct {
p *C.RegorusProgram
}
type Rvm struct {
vm *C.RegorusRvm
}
type Buffer struct {
b *C.RegorusBuffer
}
func (b *Buffer) Close() {
if b != nil && b.b != nil {
C.regorus_buffer_drop(b.b)
b.b = nil
}
}
func (b *Buffer) Bytes() []byte {
if b == nil || b.b == nil || b.b.data == nil || b.b.len == 0 {
return nil
}
return C.GoBytes(unsafe.Pointer(b.b.data), C.int(b.b.len))
}
func (p *Program) Close() {
if p != nil && p.p != nil {
C.regorus_program_drop(p.p)
p.p = nil
}
}
func (p *Program) SerializeBinary() ([]byte, error) {
result := C.regorus_program_serialize_binary(p.p)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return nil, fmt.Errorf("%s", C.GoString(result.error_message))
}
buffer := &Buffer{b: (*C.RegorusBuffer)(result.pointer_value)}
defer buffer.Close()
return buffer.Bytes(), nil
}
func (p *Program) GenerateListing() (string, error) {
result := C.regorus_program_generate_listing(p.p)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
}
func (p *Program) GenerateTabularListing() (string, error) {
result := C.regorus_program_generate_tabular_listing(p.p)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
}
func DeserializeProgram(data []byte) (*Program, bool, error) {
if len(data) == 0 {
return nil, false, fmt.Errorf("empty program data")
}
var isPartial C.bool
result := C.regorus_program_deserialize_binary((*C.uchar)(unsafe.Pointer(&data[0])), C.ulong(len(data)), (*C.bool)(unsafe.Pointer(&isPartial)))
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return nil, false, fmt.Errorf("%s", C.GoString(result.error_message))
}
return &Program{p: (*C.RegorusProgram)(result.pointer_value)}, bool(isPartial), nil
}
func CompileProgramFromModules(data string, modules []PolicyModule, entryPoints []string) (*Program, error) {
dataC := C.CString(data)
defer C.free(unsafe.Pointer(dataC))
cModules := make([]C.RegorusPolicyModule, len(modules))
moduleIdPtrs := make([]*C.char, len(modules))
moduleContentPtrs := make([]*C.char, len(modules))
for i, module := range modules {
idC := C.CString(module.Id)
contentC := C.CString(module.Content)
moduleIdPtrs[i] = idC
moduleContentPtrs[i] = contentC
cModules[i].id = idC
cModules[i].content = contentC
}
defer func() {
for i := range moduleIdPtrs {
if moduleIdPtrs[i] != nil {
C.free(unsafe.Pointer(moduleIdPtrs[i]))
}
if moduleContentPtrs[i] != nil {
C.free(unsafe.Pointer(moduleContentPtrs[i]))
}
}
}()
entryPtrs := make([]*C.char, len(entryPoints))
for i, entry := range entryPoints {
entryPtrs[i] = C.CString(entry)
}
defer func() {
for _, ptr := range entryPtrs {
C.free(unsafe.Pointer(ptr))
}
}()
var modulesPtr *C.RegorusPolicyModule
if len(cModules) > 0 {
modulesPtr = (*C.RegorusPolicyModule)(unsafe.Pointer(&cModules[0]))
}
var entryPtr **C.char
if len(entryPtrs) > 0 {
entryPtr = (**C.char)(unsafe.Pointer(&entryPtrs[0]))
}
result := C.regorus_program_compile_from_modules(
dataC,
modulesPtr,
C.ulong(len(cModules)),
entryPtr,
C.ulong(len(entryPtrs)),
)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return nil, fmt.Errorf("%s", C.GoString(result.error_message))
}
return &Program{p: (*C.RegorusProgram)(result.pointer_value)}, nil
}
func CompileProgramFromEngine(engine *Engine, entryPoints []string) (*Program, error) {
entryPtrs := make([]*C.char, len(entryPoints))
for i, entry := range entryPoints {
entryPtrs[i] = C.CString(entry)
}
defer func() {
for _, ptr := range entryPtrs {
C.free(unsafe.Pointer(ptr))
}
}()
var entryPtr **C.char
if len(entryPtrs) > 0 {
entryPtr = (**C.char)(unsafe.Pointer(&entryPtrs[0]))
}
result := C.regorus_engine_compile_program_with_entrypoints(
engine.e,
entryPtr,
C.ulong(len(entryPtrs)),
)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return nil, fmt.Errorf("%s", C.GoString(result.error_message))
}
return &Program{p: (*C.RegorusProgram)(result.pointer_value)}, nil
}
func NewRvm() (*Rvm, error) {
vm := C.regorus_rvm_new()
if vm == nil {
return nil, fmt.Errorf("failed to create RVM")
}
return &Rvm{vm: vm}, nil
}
func (r *Rvm) Close() {
if r != nil && r.vm != nil {
C.regorus_rvm_drop(r.vm)
r.vm = nil
}
}
func (r *Rvm) LoadProgram(program *Program) error {
result := C.regorus_rvm_load_program(r.vm, program.p)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (r *Rvm) SetDataJson(data string) error {
dataC := C.CString(data)
defer C.free(unsafe.Pointer(dataC))
result := C.regorus_rvm_set_data(r.vm, dataC)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (r *Rvm) SetInputJson(input string) error {
inputC := C.CString(input)
defer C.free(unsafe.Pointer(inputC))
result := C.regorus_rvm_set_input(r.vm, inputC)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (r *Rvm) SetExecutionMode(mode byte) error {
result := C.regorus_rvm_set_execution_mode(r.vm, C.uchar(mode))
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (r *Rvm) Execute() (string, error) {
result := C.regorus_rvm_execute(r.vm)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
}
func (r *Rvm) ExecuteEntryPoint(name string) (string, error) {
nameC := C.CString(name)
defer C.free(unsafe.Pointer(nameC))
result := C.regorus_rvm_execute_entry_point_by_name(r.vm, nameC)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
}
func (r *Rvm) ExecuteEntryPointIndex(index uint64) (string, error) {
result := C.regorus_rvm_execute_entry_point_by_index(r.vm, C.ulong(index))
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
}
func (r *Rvm) Resume(resumeValue string, hasValue bool) (string, error) {
var valueC *C.char
if hasValue {
valueC = C.CString(resumeValue)
defer C.free(unsafe.Pointer(valueC))
}
result := C.regorus_rvm_resume(r.vm, valueC, C.bool(hasValue))
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
}
func (r *Rvm) GetExecutionState() (string, error) {
result := C.regorus_rvm_get_execution_state(r.vm)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
}

View File

@@ -0,0 +1,128 @@
package regorus
import "testing"
const rvmPolicy = `
package demo
import rego.v1
default allow := false
allow if {
input.account.active == true
details := __builtin_host_await(input.account.id, "account")
details.tier == "gold"
}
`
const rvmInput = `{"account":{"id":"acct-1","active":true}}`
const rvmRegularPolicy = `
package demo
import rego.v1
default allow := false
allow if {
input.user == "alice"
input.active == true
}
`
const rvmRegularInput = `{"user":"alice","active":true}`
func TestRvmProgramCompileAndExecute(t *testing.T) {
modules := []PolicyModule{{Id: "demo.rego", Content: rvmRegularPolicy}}
entryPoints := []string{"data.demo.allow"}
program, err := CompileProgramFromModules("{}", modules, entryPoints)
if err != nil {
t.Fatalf("compile program: %v", err)
}
defer program.Close()
listing, err := program.GenerateListing()
if err != nil || listing == "" {
t.Fatalf("listing failed: %v", err)
}
binary, err := program.SerializeBinary()
if err != nil {
t.Fatalf("serialize program: %v", err)
}
rehydrated, isPartial, err := DeserializeProgram(binary)
if err != nil {
t.Fatalf("deserialize program: %v", err)
}
if isPartial {
t.Fatalf("deserialized program marked partial")
}
defer rehydrated.Close()
vm, err := NewRvm()
if err != nil {
t.Fatalf("new vm: %v", err)
}
defer vm.Close()
if err := vm.LoadProgram(rehydrated); err != nil {
t.Fatalf("load program: %v", err)
}
if err := vm.SetInputJson(rvmRegularInput); err != nil {
t.Fatalf("set input: %v", err)
}
result, err := vm.Execute()
if err != nil {
t.Fatalf("execute: %v", err)
}
if result != "true" {
t.Fatalf("expected allow=true, got %s", result)
}
}
func TestRvmHostAwaitSuspendResume(t *testing.T) {
modules := []PolicyModule{{Id: "host_await.rego", Content: rvmPolicy}}
entryPoints := []string{"data.demo.allow"}
program, err := CompileProgramFromModules("{}", modules, entryPoints)
if err != nil {
t.Fatalf("compile program: %v", err)
}
defer program.Close()
vm, err := NewRvm()
if err != nil {
t.Fatalf("new vm: %v", err)
}
defer vm.Close()
if err := vm.SetExecutionMode(1); err != nil {
t.Fatalf("set execution mode: %v", err)
}
if err := vm.LoadProgram(program); err != nil {
t.Fatalf("load program: %v", err)
}
if err := vm.SetInputJson(rvmInput); err != nil {
t.Fatalf("set input: %v", err)
}
if _, err := vm.Execute(); err != nil {
t.Fatalf("execute in suspendable mode failed: %v", err)
}
state, err := vm.GetExecutionState()
if err != nil {
t.Fatalf("get execution state: %v", err)
}
if state == "" {
t.Fatalf("expected non-empty execution state")
}
result, err := vm.Resume(`{"tier":"gold"}`, true)
if err != nil {
t.Fatalf("resume: %v", err)
}
if result != "true" {
t.Fatalf("expected allow=true, got %s", result)
}
}

BIN
bindings/go/regorus_test Executable file

Binary file not shown.

View File

@@ -52,6 +52,16 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"serde",
"unty",
]
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -109,9 +119,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cc"
version = "1.2.54"
version = "1.2.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -213,9 +223,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fluent-uri"
@@ -279,9 +289,9 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "iana-time-zone"
version = "0.1.64"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -411,6 +421,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown",
"serde",
"serde_core",
]
[[package]]
@@ -817,10 +829,12 @@ name = "regorus"
version = "0.9.0"
dependencies = [
"anyhow",
"bincode",
"chrono",
"chrono-tz",
"data-encoding",
"globset",
"indexmap",
"ipnet",
"jsonschema",
"lazy_static",
@@ -1067,6 +1081,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "url"
version = "2.5.8"
@@ -1364,18 +1384,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.34"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.34"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -21,4 +21,4 @@ ast = ["regorus/ast"]
anyhow = "1.0"
serde_json = "1.0.112"
jni = "0.21.1"
regorus = { path = "../..", default-features = false, features = ["arc"] }
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }

View File

@@ -2,6 +2,9 @@
// Licensed under the MIT License.
import com.microsoft.regorus.Engine;
import com.microsoft.regorus.PolicyModule;
import com.microsoft.regorus.Program;
import com.microsoft.regorus.Rvm;
public class Test {
@@ -44,5 +47,70 @@ public class Test {
"package world\nx { true }"
);
}
String regularPolicy = String.join("\n",
"package demo",
"import rego.v1",
"",
"default allow := false",
"",
"allow if {",
" input.user == \"alice\"",
" input.active == true",
"}"
);
String regularInput = "{\"user\":\"alice\",\"active\":true}";
{
PolicyModule module = new PolicyModule("demo.rego", regularPolicy);
Program program = Program.compileFromModules("{}", new PolicyModule[]{module}, new String[]{"data.demo.allow"});
System.out.println("RVM listing:\n" + program.generateListing());
byte[] binary = program.serializeBinary();
program.close();
boolean[] isPartial = new boolean[1];
Program rehydrated = Program.deserializeBinary(binary, isPartial);
if (isPartial[0]) {
throw new IllegalStateException("Deserialized program marked partial");
}
try (Rvm vm = new Rvm()) {
vm.loadProgram(rehydrated);
vm.setInputJson(regularInput);
String result = vm.execute();
System.out.println("RVM regular result: " + result);
}
rehydrated.close();
}
String awaitPolicy = String.join("\n",
"package demo",
"import rego.v1",
"",
"default allow := false",
"",
"allow if {",
" input.account.active == true",
" details := __builtin_host_await(input.account.id, \"account\")",
" details.tier == \"gold\"",
"}"
);
String awaitInput = "{\"account\":{\"id\":\"acct-1\",\"active\":true}}";
{
PolicyModule module = new PolicyModule("await.rego", awaitPolicy);
Program program = Program.compileFromModules("{}", new PolicyModule[]{module}, new String[]{"data.demo.allow"});
try (Rvm vm = new Rvm()) {
vm.setExecutionMode((byte) 1);
vm.loadProgram(program);
vm.setInputJson(awaitInput);
vm.execute();
System.out.println("HostAwait state: " + vm.getExecutionState());
String resumed = vm.resume("{\"tier\":\"gold\"}");
System.out.println("HostAwait result: " + resumed);
}
program.close();
}
}
}

View File

@@ -2,11 +2,17 @@
// Licensed under the MIT License.
use anyhow::Result;
use jni::objects::{JClass, JObject, JString};
use jni::sys::{jlong, jstring};
use jni::objects::{JBooleanArray, JByteArray, JClass, JObject, JObjectArray, JString};
use jni::sys::{jboolean, jbooleanArray, jbyteArray, jlong, jobjectArray, jstring};
use jni::JNIEnv;
use regorus::{Engine, Value};
use regorus::languages::rego::compiler::Compiler;
use regorus::rvm::program::{
generate_assembly_listing, AssemblyListingConfig, DeserializationResult, Program as RvmProgram,
};
use regorus::rvm::vm::{ExecutionMode, RegoVM};
use regorus::{compile_policy_with_entrypoint, Engine, PolicyModule, Rc, Value};
use std::sync::Arc;
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeNewEngine(
@@ -370,6 +376,336 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeDestroyEngine(
}
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromModules(
env: JNIEnv,
_class: JClass,
data_json: JString,
module_ids: jobjectArray,
module_contents: jobjectArray,
entry_points: jobjectArray,
) -> jlong {
let res = throw_err(env, |env| {
let data_json: String = env.get_string(&data_json)?.into();
let data = Value::from_json_str(&data_json)?;
let ids = get_string_array(env, module_ids)?;
let contents = get_string_array(env, module_contents)?;
if ids.len() != contents.len() {
return Err(anyhow::anyhow!("module id/content length mismatch"));
}
let mut modules = Vec::with_capacity(ids.len());
for (id, content) in ids.into_iter().zip(contents.into_iter()) {
modules.push(PolicyModule {
id: Rc::from(id.as_str()),
content: Rc::from(content.as_str()),
});
}
let entry_points_vec = get_string_array(env, entry_points)?;
if entry_points_vec.is_empty() {
return Err(anyhow::anyhow!(
"entry_points must contain at least one entry"
));
}
let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect();
let entry_rule = entry_points_ref[0];
let compiled = compile_policy_with_entrypoint(data, &modules, Rc::from(entry_rule))?;
let program = Compiler::compile_from_policy(&compiled, &entry_points_ref)?;
Ok(Box::into_raw(Box::new(program)) as jlong)
});
res.unwrap_or_default()
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromEngine(
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
entry_points: jobjectArray,
) -> jlong {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let entry_points_vec = get_string_array(env, entry_points)?;
if entry_points_vec.is_empty() {
return Err(anyhow::anyhow!(
"entry_points must contain at least one entry"
));
}
let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect();
let entry_rule = Rc::from(entry_points_ref[0]);
let compiled = engine.compile_with_entrypoint(&entry_rule)?;
let program = Compiler::compile_from_policy(&compiled, &entry_points_ref)?;
Ok(Box::into_raw(Box::new(program)) as jlong)
});
res.unwrap_or_default()
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeGenerateListing(
env: JNIEnv,
_class: JClass,
program_ptr: jlong,
) -> jstring {
let res = throw_err(env, |env| {
let program = unsafe { &*(program_ptr as *mut Arc<RvmProgram>) };
let listing =
generate_assembly_listing(program.as_ref(), &AssemblyListingConfig::default());
let output = env.new_string(&listing)?;
Ok(output.into_raw())
});
match res {
Ok(val) => val,
Err(_) => JObject::null().into_raw(),
}
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeSerializeBinary(
env: JNIEnv,
_class: JClass,
program_ptr: jlong,
) -> jbyteArray {
let res = throw_err(env, |env| {
let program = unsafe { &*(program_ptr as *mut Arc<RvmProgram>) };
let bytes = program.serialize_binary().map_err(|e| anyhow::anyhow!(e))?;
let array = env.byte_array_from_slice(&bytes)?;
Ok(array.into_raw())
});
match res {
Ok(val) => val,
Err(_) => JObject::null().into_raw(),
}
}
#[no_mangle]
/// # Safety
///
/// The `data` and `is_partial` pointers must be valid JNI array references
/// for the duration of the call. They must come from the JVM for the current
/// thread and not be used after this function returns.
pub unsafe extern "system" fn Java_com_microsoft_regorus_Program_nativeDeserializeBinary(
env: JNIEnv,
_class: JClass,
data: jbyteArray,
is_partial: jbooleanArray,
) -> jlong {
let res = throw_err(env, |env| {
if data.is_null() {
return Err(anyhow::anyhow!("data must not be null"));
}
let data = unsafe { JByteArray::from_raw(data) };
let bytes = env.convert_byte_array(&data)?;
let (program, partial) =
match RvmProgram::deserialize_binary(&bytes).map_err(|e| anyhow::anyhow!(e))? {
DeserializationResult::Complete(program) => (program, false),
DeserializationResult::Partial(program) => (program, true),
};
if !is_partial.is_null() {
let is_partial = unsafe { JBooleanArray::from_raw(is_partial) };
let len = env.get_array_length(&is_partial)?;
if len > 0 {
let value: [jboolean; 1] = [if partial { 1 } else { 0 }];
env.set_boolean_array_region(&is_partial, 0, &value)?;
}
}
Ok(Box::into_raw(Box::new(Arc::new(program))) as jlong)
});
res.unwrap_or_default()
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeDrop(
_env: JNIEnv,
_class: JClass,
program_ptr: jlong,
) {
unsafe {
let _program = Box::from_raw(program_ptr as *mut Arc<RvmProgram>);
}
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeNew(
_env: JNIEnv,
_class: JClass,
) -> jlong {
let vm = RegoVM::new();
Box::into_raw(Box::new(vm)) as jlong
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeLoadProgram(
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
program_ptr: jlong,
) {
let _ = throw_err(env, |_env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let program = unsafe { &*(program_ptr as *mut Arc<RvmProgram>) };
vm.load_program(program.clone());
Ok(())
});
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetDataJson(
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
data_json: JString,
) {
let _ = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let data_json: String = env.get_string(&data_json)?.into();
let data = Value::from_json_str(&data_json)?;
vm.set_data(data)?;
Ok(())
});
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetInputJson(
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
input_json: JString,
) {
let _ = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let input_json: String = env.get_string(&input_json)?.into();
let input = Value::from_json_str(&input_json)?;
vm.set_input(input);
Ok(())
});
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetExecutionMode(
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
mode: u8,
) {
let _ = throw_err(env, |_env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let mode = match mode {
0 => ExecutionMode::RunToCompletion,
1 => ExecutionMode::Suspendable,
_ => return Err(anyhow::anyhow!("invalid execution mode")),
};
vm.set_execution_mode(mode);
Ok(())
});
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecute(
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
) -> jstring {
let res = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let result = vm.execute()?;
let output = env.new_string(result.to_json_str()?)?;
Ok(output.into_raw())
});
match res {
Ok(val) => val,
Err(_) => JObject::null().into_raw(),
}
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecuteEntryPoint(
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
entry_point: JString,
) -> jstring {
let res = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let entry_point: String = env.get_string(&entry_point)?.into();
let result = vm.execute_entry_point_by_name(&entry_point)?;
let output = env.new_string(result.to_json_str()?)?;
Ok(output.into_raw())
});
match res {
Ok(val) => val,
Err(_) => JObject::null().into_raw(),
}
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeResume(
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
resume_json: JString,
has_value: bool,
) -> jstring {
let res = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let value = if has_value {
let resume_json: String = env.get_string(&resume_json)?.into();
Some(Value::from_json_str(&resume_json)?)
} else {
None
};
let result = vm.resume(value)?;
let output = env.new_string(result.to_json_str()?)?;
Ok(output.into_raw())
});
match res {
Ok(val) => val,
Err(_) => JObject::null().into_raw(),
}
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeGetExecutionState(
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
) -> jstring {
let res = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let output = env.new_string(format!("{:?}", vm.execution_state()))?;
Ok(output.into_raw())
});
match res {
Ok(val) => val,
Err(_) => JObject::null().into_raw(),
}
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeDrop(
_env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
) {
unsafe {
let _vm = Box::from_raw(vm_ptr as *mut RegoVM);
}
}
fn throw_err<T>(mut env: JNIEnv, mut f: impl FnMut(&mut JNIEnv) -> Result<T>) -> Result<T> {
match f(&mut env) {
Ok(val) => Ok(val),
@@ -379,3 +715,19 @@ fn throw_err<T>(mut env: JNIEnv, mut f: impl FnMut(&mut JNIEnv) -> Result<T>) ->
}
}
}
fn get_string_array(env: &mut JNIEnv, array: jobjectArray) -> Result<Vec<String>> {
if array.is_null() {
return Ok(Vec::new());
}
let array = unsafe { JObjectArray::from_raw(array) };
let len = env.get_array_length(&array)?;
let mut values = Vec::with_capacity(len as usize);
for i in 0..len {
let obj = env.get_object_array_element(&array, i)?;
let jstr = JString::from(obj);
let value: String = env.get_string(&jstr)?.into();
values.push(value);
}
Ok(values)
}

View File

@@ -221,6 +221,8 @@ public class Engine implements AutoCloseable, Cloneable {
/**
* Get coverage report as json string.
*
* @return Coverage report as a JSON string.
*
*/
public String getCoverageReport() {
@@ -229,6 +231,8 @@ public class Engine implements AutoCloseable, Cloneable {
/**
* Get coverage report as ANSI color coded string.
*
* @return Coverage report formatted for console output.
*
*/
public String getCoverageReportPretty() {
@@ -247,12 +251,18 @@ public class Engine implements AutoCloseable, Cloneable {
/**
* Take gathered prints.
*
* @return Collected print output as JSON.
*
*/
public String takePrints() {
return nativeTakePrints(enginePtr);
}
long getPtr() {
return enginePtr;
}
@Override
public void close() {

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
**/
package com.microsoft.regorus;
/**
* Represents a Rego module used for RVM program compilation.
*/
public final class PolicyModule {
/**
* Module identifier or filename.
*/
public final String id;
/**
* Rego policy content.
*/
public final String content;
/**
* Create a new policy module.
*
* @param id Module identifier or filename.
* @param content Rego policy content.
*/
public PolicyModule(String id, String content) {
this.id = id;
this.content = content;
}
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
**/
package com.microsoft.regorus;
/**
* Represents a compiled RVM program.
*/
public final class Program implements AutoCloseable {
private static native long nativeCompileFromModules(
String dataJson,
String[] moduleIds,
String[] moduleContents,
String[] entryPoints);
private static native long nativeCompileFromEngine(long enginePtr, String[] entryPoints);
private static native String nativeGenerateListing(long programPtr);
private static native byte[] nativeSerializeBinary(long programPtr);
private static native long nativeDeserializeBinary(byte[] data, boolean[] isPartial);
private static native void nativeDrop(long programPtr);
private final long programPtr;
Program(long ptr) {
this.programPtr = ptr;
}
/**
* Compile a program from modules and entry points.
*
* @param dataJson JSON document to merge as static data.
* @param modules Policy modules to compile.
* @param entryPoints Entry point rule paths.
* @return Compiled program instance.
*/
public static Program compileFromModules(String dataJson, PolicyModule[] modules, String[] entryPoints) {
String[] ids = new String[modules.length];
String[] contents = new String[modules.length];
for (int i = 0; i < modules.length; i++) {
ids[i] = modules[i].id;
contents[i] = modules[i].content;
}
long ptr = nativeCompileFromModules(dataJson, ids, contents, entryPoints);
return new Program(ptr);
}
/**
* Compile a program from an engine and entry points.
*
* @param engine Engine with loaded policies.
* @param entryPoints Entry point rule paths.
* @return Compiled program instance.
*/
public static Program compileFromEngine(Engine engine, String[] entryPoints) {
long ptr = nativeCompileFromEngine(engine.getPtr(), entryPoints);
return new Program(ptr);
}
/**
* Generate a readable assembly listing.
*
* @return Listing text.
*/
public String generateListing() {
return nativeGenerateListing(programPtr);
}
/**
* Serialize the program to binary format.
*
* @return Serialized bytes.
*/
public byte[] serializeBinary() {
return nativeSerializeBinary(programPtr);
}
/**
* Deserialize a program from binary format.
*
* @param data Serialized program bytes.
* @param isPartial Optional array to receive the partial flag (index 0).
* @return Deserialized program instance.
*/
public static Program deserializeBinary(byte[] data, boolean[] isPartial) {
if (data == null || data.length == 0) {
throw new IllegalArgumentException("data must not be empty");
}
long ptr = nativeDeserializeBinary(data, isPartial);
return new Program(ptr);
}
long getPtr() {
return programPtr;
}
@Override
public void close() {
nativeDrop(programPtr);
}
}

View File

@@ -0,0 +1,110 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
**/
package com.microsoft.regorus;
/**
* Wrapper for the Regorus RVM runtime.
*/
public final class Rvm implements AutoCloseable {
private static native long nativeNew();
private static native void nativeDrop(long vmPtr);
private static native void nativeLoadProgram(long vmPtr, long programPtr);
private static native void nativeSetDataJson(long vmPtr, String dataJson);
private static native void nativeSetInputJson(long vmPtr, String inputJson);
private static native void nativeSetExecutionMode(long vmPtr, byte mode);
private static native String nativeExecute(long vmPtr);
private static native String nativeExecuteEntryPoint(long vmPtr, String entryPoint);
private static native String nativeResume(long vmPtr, String resumeJson, boolean hasValue);
private static native String nativeGetExecutionState(long vmPtr);
private final long vmPtr;
/**
* Create a new RVM instance.
*/
public Rvm() {
this.vmPtr = nativeNew();
}
/**
* Load a program into the VM.
*
* @param program Compiled program.
*/
public void loadProgram(Program program) {
nativeLoadProgram(vmPtr, program.getPtr());
}
/**
* Set data JSON for the VM.
*
* @param dataJson JSON data document.
*/
public void setDataJson(String dataJson) {
nativeSetDataJson(vmPtr, dataJson);
}
/**
* Set input JSON for the VM.
*
* @param inputJson JSON input document.
*/
public void setInputJson(String inputJson) {
nativeSetInputJson(vmPtr, inputJson);
}
/**
* Set execution mode (0 = run-to-completion, 1 = suspendable).
*
* @param mode Execution mode.
*/
public void setExecutionMode(byte mode) {
nativeSetExecutionMode(vmPtr, mode);
}
/**
* Execute the program.
*
* @return JSON result string.
*/
public String execute() {
return nativeExecute(vmPtr);
}
/**
* Execute a named entry point.
*
* @param entryPoint Entry point rule path.
* @return JSON result string.
*/
public String executeEntryPoint(String entryPoint) {
return nativeExecuteEntryPoint(vmPtr, entryPoint);
}
/**
* Resume execution with an optional JSON value.
*
* @param resumeJson JSON value to resume with, or null for no value.
* @return JSON result string.
*/
public String resume(String resumeJson) {
return nativeResume(vmPtr, resumeJson, resumeJson != null);
}
/**
* Get the current execution state.
*
* @return Execution state string.
*/
public String getExecutionState() {
return nativeGetExecutionState(vmPtr);
}
@Override
public void close() {
nativeDrop(vmPtr);
}
}

View File

@@ -52,6 +52,16 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"serde",
"unty",
]
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -103,9 +113,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]]
name = "cc"
version = "1.2.54"
version = "1.2.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -191,9 +201,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fluent-uri"
@@ -263,9 +273,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.64"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -395,6 +405,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown",
"serde",
"serde_core",
]
[[package]]
@@ -876,10 +888,12 @@ name = "regorus"
version = "0.9.0"
dependencies = [
"anyhow",
"bincode",
"chrono",
"chrono-tz",
"data-encoding",
"globset",
"indexmap",
"ipnet",
"jsonschema",
"lazy_static",
@@ -1110,6 +1124,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "url"
version = "2.5.8"
@@ -1313,18 +1333,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.34"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.34"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -22,6 +22,6 @@ coverage = ["regorus/coverage"]
anyhow = "1.0"
ordered-float = "5.0.0"
pyo3 = { version = "0.24.1", features = ["abi3-py310", "anyhow", "extension-module"] }
regorus = { path = "../..", default-features = false, features = ["arc"] }
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }
serde_json = "1.0.140"

View File

@@ -8,7 +8,14 @@ use pyo3::IntoPyObjectExt;
use std::collections::{BTreeMap, BTreeSet};
use ::regorus::Value;
use ::regorus::languages::rego::compiler::Compiler;
use ::regorus::rvm::program::{
generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig,
DeserializationResult, Program as RvmProgram,
};
use ::regorus::rvm::vm::{ExecutionMode, RegoVM};
use ::regorus::{compile_policy_with_entrypoint, PolicyModule, Rc, Value};
use std::sync::Arc;
/// Regorus engine.
#[pyclass(unsendable)]
@@ -16,6 +23,18 @@ pub struct Engine {
engine: ::regorus::Engine,
}
/// RVM program wrapper.
#[pyclass(unsendable)]
pub struct Program {
program: Arc<RvmProgram>,
}
/// RVM runtime wrapper.
#[pyclass(unsendable)]
pub struct Rvm {
vm: RegoVM,
}
impl Default for Engine {
fn default() -> Self {
Self::new()
@@ -384,7 +403,151 @@ impl Engine {
}
}
#[pymethods]
impl Program {
/// Compile an RVM program from modules and entry points.
#[staticmethod]
pub fn compile_from_modules(
data_json: String,
modules: Vec<(String, String)>,
entry_points: Vec<String>,
) -> Result<Self> {
if entry_points.is_empty() {
return Err(anyhow!("entry_points must contain at least one entry"));
}
let data = Value::from_json_str(&data_json)?;
let policy_modules: Vec<PolicyModule> = modules
.into_iter()
.map(|(id, content)| PolicyModule {
id: Rc::from(id.as_str()),
content: Rc::from(content.as_str()),
})
.collect();
let entry_points_ref: Vec<&str> = entry_points.iter().map(|s| s.as_str()).collect();
let entry_rule = Rc::from(entry_points_ref[0]);
let compiled = compile_policy_with_entrypoint(data, &policy_modules, entry_rule)?;
let program = Compiler::compile_from_policy(&compiled, &entry_points_ref)?;
Ok(Self { program })
}
/// Deserialize an RVM program from binary data.
#[staticmethod]
pub fn deserialize_binary(data: Vec<u8>) -> Result<(Self, bool)> {
let (program, is_partial) =
match RvmProgram::deserialize_binary(&data).map_err(|e: String| anyhow!(e))? {
DeserializationResult::Complete(program) => (program, false),
DeserializationResult::Partial(program) => (program, true),
};
Ok((
Self {
program: Arc::new(program),
},
is_partial,
))
}
/// Serialize a program to binary format.
pub fn serialize_binary(&self) -> Result<Vec<u8>> {
self.program
.serialize_binary()
.map_err(|e: String| anyhow!(e))
}
/// Generate a readable assembly listing.
pub fn generate_listing(&self) -> Result<String> {
Ok(generate_assembly_listing(
self.program.as_ref(),
&AssemblyListingConfig::default(),
))
}
/// Generate a tabular assembly listing.
pub fn generate_tabular_listing(&self) -> Result<String> {
Ok(generate_tabular_assembly_listing(
self.program.as_ref(),
&AssemblyListingConfig::default(),
))
}
}
impl Default for Rvm {
fn default() -> Self {
Self::new()
}
}
#[pymethods]
impl Rvm {
#[new]
pub fn new() -> Self {
Self { vm: RegoVM::new() }
}
/// Load an RVM program into the VM.
pub fn load_program(&mut self, program: &Program) -> Result<()> {
self.vm.load_program(program.program.clone());
Ok(())
}
/// Set data JSON for the VM.
pub fn set_data_json(&mut self, data_json: String) -> Result<()> {
let data = Value::from_json_str(&data_json)?;
self.vm.set_data(data)?;
Ok(())
}
/// Set input JSON for the VM.
pub fn set_input_json(&mut self, input_json: String) -> Result<()> {
let input = Value::from_json_str(&input_json)?;
self.vm.set_input(input);
Ok(())
}
/// Set execution mode (0 = run-to-completion, 1 = suspendable).
pub fn set_execution_mode(&mut self, mode: u8) -> Result<()> {
let mode = match mode {
0 => ExecutionMode::RunToCompletion,
1 => ExecutionMode::Suspendable,
_ => return Err(anyhow!("invalid execution mode")),
};
self.vm.set_execution_mode(mode);
Ok(())
}
/// Execute the program and return the JSON result.
pub fn execute(&mut self) -> Result<String> {
self.vm.execute()?.to_json_str()
}
/// Execute an entry point by name and return the JSON result.
pub fn execute_entry_point(&mut self, entry_point: String) -> Result<String> {
self.vm
.execute_entry_point_by_name(&entry_point)?
.to_json_str()
}
/// Resume execution with an optional JSON value.
pub fn resume(&mut self, resume_json: Option<String>) -> Result<String> {
let value = if let Some(json) = resume_json {
Some(Value::from_json_str(&json)?)
} else {
None
};
self.vm.resume(value)?.to_json_str()
}
/// Get the execution state as a string.
pub fn get_execution_state(&self) -> Result<String> {
Ok(format!("{:?}", self.vm.execution_state()))
}
}
#[pymodule]
pub fn regorus(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<crate::Engine>()
m.add_class::<crate::Engine>()?;
m.add_class::<crate::Program>()?;
m.add_class::<crate::Rvm>()?;
Ok(())
}

View File

@@ -2,6 +2,10 @@
# Licensed under the MIT License.
import regorus
import sys
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
# Create engine
engine = regorus.Engine()
@@ -94,3 +98,68 @@ engine1.set_gather_prints(True)
engine1.eval_query('print("Hello")')
ps = engine1.take_prints()
print(ps)
# RVM regular example
policy = """
package demo
import rego.v1
default allow := false
allow if {
input.user == "alice"
input.active == true
}
"""
def run_regular_example():
module = ("demo.rego", policy)
program = regorus.Program.compile_from_modules(
"{}",
[module],
["data.demo.allow"],
)
print(program.generate_listing())
binary = program.serialize_binary()
program, is_partial = regorus.Program.deserialize_binary(binary)
if is_partial:
raise RuntimeError("Deserialized program marked partial")
vm = regorus.Rvm()
vm.load_program(program)
vm.set_input_json('{"user":"alice","active":true}')
print(vm.execute())
run_regular_example()
# RVM HostAwait example
policy = """
package demo
import rego.v1
default allow := false
allow if {
input.account.active == true
details := __builtin_host_await(input.account.id, "account")
details.tier == "gold"
}
"""
def run_host_await_example():
module = ("await.rego", policy)
program = regorus.Program.compile_from_modules(
"{}",
[module],
["data.demo.allow"],
)
vm = regorus.Rvm()
vm.set_execution_mode(1)
vm.load_program(program)
vm.set_input_json('{"account":{"id":"acct-1","active":true}}')
vm.execute()
print(vm.get_execution_state())
print(vm.resume('{"tier":"gold"}'))
run_host_await_example()

View File

@@ -63,6 +63,16 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"serde",
"unty",
]
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -120,9 +130,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.54"
version = "1.2.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -208,9 +218,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fluent-uri"
@@ -314,9 +324,9 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "iana-time-zone"
version = "0.1.64"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -446,6 +456,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown",
"serde",
"serde_core",
]
[[package]]
@@ -874,10 +886,12 @@ name = "regorus"
version = "0.9.0"
dependencies = [
"anyhow",
"bincode",
"chrono",
"chrono-tz",
"data-encoding",
"globset",
"indexmap",
"ipnet",
"jsonschema",
"lazy_static",
@@ -903,6 +917,7 @@ dependencies = [
"getrandom 0.2.17",
"getrandom 0.3.4",
"regorus",
"serde",
"serde_json",
"uuid",
"wasm-bindgen",
@@ -1098,6 +1113,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "url"
version = "2.5.8"
@@ -1398,18 +1419,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.34"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.34"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -37,7 +37,8 @@ ast = ["regorus/ast"]
coverage = ["regorus/coverage"]
[dependencies]
regorus = { path = "../..", default-features = false, features = ["arc"] }
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
wasm-bindgen = "0.2.100"
# Specify uuid as a mandatory dependency so as to enable `js` feature which is now required

View File

@@ -3,6 +3,15 @@
#![allow(non_snake_case)]
use regorus::languages::rego::compiler::Compiler;
use regorus::rvm::program::{
generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig,
DeserializationResult, Program as RvmProgram,
};
use regorus::rvm::vm::{ExecutionMode, RegoVM};
use regorus::{compile_policy_with_entrypoint, PolicyModule, Rc, Value};
use serde::Deserialize;
use std::sync::Arc;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
@@ -11,6 +20,44 @@ pub struct Engine {
engine: regorus::Engine,
}
#[derive(Deserialize)]
struct ModuleSpec {
id: String,
content: String,
}
#[wasm_bindgen]
pub struct Program {
program: Arc<RvmProgram>,
}
#[wasm_bindgen]
pub struct ProgramDeserializationResult {
program: Arc<RvmProgram>,
is_partial: bool,
}
#[wasm_bindgen]
impl ProgramDeserializationResult {
/// Whether the program was partially deserialized.
#[wasm_bindgen(getter)]
pub fn isPartial(&self) -> bool {
self.is_partial
}
/// Get the deserialized program.
pub fn program(&self) -> Program {
Program {
program: self.program.clone(),
}
}
}
#[wasm_bindgen]
pub struct Rvm {
vm: RegoVM,
}
fn error_to_jsvalue<E: std::fmt::Display>(e: E) -> JsValue {
JsValue::from_str(&format!("{e}"))
}
@@ -193,6 +240,153 @@ impl Engine {
}
}
#[wasm_bindgen]
impl Program {
/// Compile an RVM program from modules and entry points.
pub fn compileFromModules(
data_json: String,
modules_json: String,
entry_points_json: String,
) -> Result<Program, JsValue> {
let data = Value::from_json_str(&data_json).map_err(error_to_jsvalue)?;
let modules: Vec<ModuleSpec> =
serde_json::from_str(&modules_json).map_err(error_to_jsvalue)?;
let entry_points: Vec<String> =
serde_json::from_str(&entry_points_json).map_err(error_to_jsvalue)?;
if entry_points.is_empty() {
return Err(error_to_jsvalue(
"entry_points must contain at least one entry",
));
}
let policy_modules: Vec<PolicyModule> = modules
.into_iter()
.map(|module| PolicyModule {
id: Rc::from(module.id.as_str()),
content: Rc::from(module.content.as_str()),
})
.collect();
let entry_points_ref: Vec<&str> = entry_points.iter().map(|s| s.as_str()).collect();
let compiled =
compile_policy_with_entrypoint(data, &policy_modules, Rc::from(entry_points_ref[0]))
.map_err(error_to_jsvalue)?;
let program = Compiler::compile_from_policy(&compiled, &entry_points_ref)
.map_err(error_to_jsvalue)?;
Ok(Program { program })
}
/// Serialize a program to binary format.
pub fn serializeBinary(&self) -> Result<Vec<u8>, JsValue> {
self.program
.serialize_binary()
.map_err(|e| error_to_jsvalue(e.to_string()))
}
/// Deserialize an RVM program from binary format.
pub fn deserializeBinary(data: Vec<u8>) -> Result<ProgramDeserializationResult, JsValue> {
let (program, is_partial) =
match RvmProgram::deserialize_binary(&data).map_err(error_to_jsvalue)? {
DeserializationResult::Complete(program) => (program, false),
DeserializationResult::Partial(program) => (program, true),
};
Ok(ProgramDeserializationResult {
program: Arc::new(program),
is_partial,
})
}
/// Generate a readable assembly listing.
pub fn generateListing(&self) -> Result<String, JsValue> {
Ok(generate_assembly_listing(
self.program.as_ref(),
&AssemblyListingConfig::default(),
))
}
/// Generate a tabular assembly listing.
pub fn generateTabularListing(&self) -> Result<String, JsValue> {
Ok(generate_tabular_assembly_listing(
self.program.as_ref(),
&AssemblyListingConfig::default(),
))
}
}
#[wasm_bindgen]
impl Rvm {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self { vm: RegoVM::new() }
}
/// Load a program into the VM.
pub fn loadProgram(&mut self, program: &Program) {
self.vm.load_program(program.program.clone());
}
/// Set VM data from JSON.
pub fn setDataJson(&mut self, data_json: String) -> Result<(), JsValue> {
let data = Value::from_json_str(&data_json).map_err(error_to_jsvalue)?;
self.vm.set_data(data).map_err(error_to_jsvalue)
}
/// Set VM input from JSON.
pub fn setInputJson(&mut self, input_json: String) -> Result<(), JsValue> {
let input = Value::from_json_str(&input_json).map_err(error_to_jsvalue)?;
self.vm.set_input(input);
Ok(())
}
/// Set execution mode (0 = run-to-completion, 1 = suspendable).
pub fn setExecutionMode(&mut self, mode: u8) -> Result<(), JsValue> {
let mode = match mode {
0 => ExecutionMode::RunToCompletion,
1 => ExecutionMode::Suspendable,
_ => return Err(error_to_jsvalue("invalid execution mode")),
};
self.vm.set_execution_mode(mode);
Ok(())
}
/// Execute the program and return the JSON result.
pub fn execute(&mut self) -> Result<String, JsValue> {
let value = self.vm.execute().map_err(error_to_jsvalue)?;
value.to_json_str().map_err(error_to_jsvalue)
}
/// Execute an entry point by name and return the JSON result.
pub fn executeEntryPoint(&mut self, entry_point: String) -> Result<String, JsValue> {
let value = self
.vm
.execute_entry_point_by_name(&entry_point)
.map_err(error_to_jsvalue)?;
value.to_json_str().map_err(error_to_jsvalue)
}
/// Resume execution with an optional JSON value.
pub fn resume(&mut self, resume_json: Option<String>) -> Result<String, JsValue> {
let value = if let Some(json) = resume_json {
Some(Value::from_json_str(&json).map_err(error_to_jsvalue)?)
} else {
None
};
let result = self.vm.resume(value).map_err(error_to_jsvalue)?;
result.to_json_str().map_err(error_to_jsvalue)
}
/// Get the execution state as a string.
pub fn getExecutionState(&self) -> String {
format!("{:?}", self.vm.execution_state())
}
}
impl Default for Rvm {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use crate::error_to_jsvalue;

View File

@@ -85,3 +85,78 @@ console.log(report);
// Print pretty report.
report = engine.getCoverageReportPretty();
console.log(report);
// RVM regular example
{
const policy = `
package demo
import rego.v1
default allow := false
allow if {
input.user == "alice"
input.active == true
}
`;
const modules = JSON.stringify([
{ id: "demo.rego", content: policy }
]);
const entryPoints = JSON.stringify(["data.demo.allow"]);
const program = regorus.Program.compileFromModules(
"{}",
modules,
entryPoints
);
console.log(program.generateListing());
const binary = program.serializeBinary();
const deserialized = regorus.Program.deserializeBinary(binary);
if (deserialized.isPartial) {
throw new Error("Deserialized program marked partial");
}
const rehydrated = deserialized.program();
const vm = new regorus.Rvm();
vm.loadProgram(rehydrated);
vm.setInputJson('{"user":"alice","active":true}');
console.log(vm.execute());
}
// RVM HostAwait example
{
const policy = `
package demo
import rego.v1
default allow := false
allow if {
input.account.active == true
details := __builtin_host_await(input.account.id, "account")
details.tier == "gold"
}
`;
const modules = JSON.stringify([
{ id: "await.rego", content: policy }
]);
const entryPoints = JSON.stringify(["data.demo.allow"]);
const program = regorus.Program.compileFromModules(
"{}",
modules,
entryPoints
);
const vm = new regorus.Rvm();
vm.setExecutionMode(1);
vm.loadProgram(program);
vm.setInputJson('{"account":{"id":"acct-1","active":true}}');
vm.execute();
console.log(vm.getExecutionState());
console.log(vm.resume('{"tier":"gold"}'));
}