diff --git a/bindings/cpp/regorus.hpp b/bindings/cpp/regorus.hpp index 191e4cf..554083d 100644 --- a/bindings/cpp/regorus.hpp +++ b/bindings/cpp/regorus.hpp @@ -80,6 +80,10 @@ namespace regorus { return std::unique_ptr(new Engine(regorus_engine_clone(engine))); } + Result prepare() { + return Result(regorus_engine_prepare(engine)); + } + Result set_rego_v0(bool enable) { return Result(regorus_engine_set_rego_v0(engine, enable)); } diff --git a/bindings/csharp/Regorus/Engine.cs b/bindings/csharp/Regorus/Engine.cs index 852be97..9d420b0 100644 --- a/bindings/csharp/Regorus/Engine.cs +++ b/bindings/csharp/Regorus/Engine.cs @@ -68,6 +68,18 @@ namespace Regorus }); } + /// + /// Prepare internal evaluation structures without executing a query. + /// This is optional: if skipped, the first evaluation pays this setup cost. + /// + public void Prepare() + { + UseHandle(enginePtr => + { + CheckAndDropResult(Regorus.Internal.API.regorus_engine_prepare((Regorus.Internal.RegorusEngine*)enginePtr)); + }); + } + public void SetStrictBuiltinErrors(bool strict) { UseHandle(enginePtr => diff --git a/bindings/csharp/Regorus/NativeMethods.cs b/bindings/csharp/Regorus/NativeMethods.cs index c7a071b..a7339f3 100644 --- a/bindings/csharp/Regorus/NativeMethods.cs +++ b/bindings/csharp/Regorus/NativeMethods.cs @@ -92,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); + /// + /// Prepare a RegorusEngine for evaluation without executing a query. + /// + [DllImport(LibraryName, EntryPoint = "regorus_engine_prepare", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_engine_prepare(RegorusEngine* engine); + /// /// Compile an RVM program from the engine state with entry points. /// diff --git a/bindings/ffi/src/engine.rs b/bindings/ffi/src/engine.rs index 8079b8e..8ef15d7 100644 --- a/bindings/ffi/src/engine.rs +++ b/bindings/ffi/src/engine.rs @@ -199,6 +199,21 @@ pub extern "C" fn regorus_engine_clone(engine: *mut RegorusEngine) -> *mut Regor } } +/// Prepare a [`RegorusEngine`] for evaluation without executing a query. +/// +/// This is optional. If not called, first eval performs the same setup. +/// If policy/data changes after preparation, setup is invalidated. +#[no_mangle] +pub extern "C" fn regorus_engine_prepare(engine: *mut RegorusEngine) -> RegorusResult { + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let engine = to_ref(engine)?; + let mut guard = engine.try_write()?; + guard.prepare() + }()) + }) +} + #[no_mangle] pub extern "C" fn regorus_engine_drop(engine: *mut RegorusEngine) { if let Ok(e) = to_ref(engine) { diff --git a/bindings/go/pkg/regorus/mod.go b/bindings/go/pkg/regorus/mod.go index 042f2b3..b75c4ab 100644 --- a/bindings/go/pkg/regorus/mod.go +++ b/bindings/go/pkg/regorus/mod.go @@ -28,6 +28,17 @@ func (e *Engine) Clone() *Engine { return c } +func (e *Engine) Prepare() error { + result := C.regorus_engine_prepare(e.e) + defer C.regorus_result_drop(result) + + if result.status != C.Ok { + return fmt.Errorf("%s", C.GoString(result.error_message)) + } + + return nil +} + func (e *Engine) SetRegoV0(enable bool) error { result := C.regorus_engine_set_rego_v0(e.e, C.bool(enable)) defer C.regorus_result_drop(result) diff --git a/bindings/java/com_microsoft_regorus_Engine.h b/bindings/java/com_microsoft_regorus_Engine.h index ec50788..9eec758 100644 --- a/bindings/java/com_microsoft_regorus_Engine.h +++ b/bindings/java/com_microsoft_regorus_Engine.h @@ -23,6 +23,14 @@ JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_nativeNewEngine JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_nativeClone (JNIEnv *, jclass, jlong); +/* + * Class: com_microsoft_regorus_Engine + * Method: nativePrepare + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_nativePrepare + (JNIEnv *, jclass, jlong); + /* * Class: com_microsoft_regorus_Engine * Method: nativeAddPolicy diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs index 9bd6439..2516b1a 100644 --- a/bindings/java/src/lib.rs +++ b/bindings/java/src/lib.rs @@ -36,6 +36,19 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClone( Box::into_raw(Box::new(c)) as jlong } +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Engine_nativePrepare( + env: EnvUnowned, + _class: JClass, + engine_ptr: jlong, +) { + let _ = throw_err(env, |_env| { + let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; + engine.prepare()?; + Ok(()) + }); +} + #[no_mangle] pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetRegoV0( env: EnvUnowned, diff --git a/bindings/java/src/main/java/com/microsoft/regorus/Engine.java b/bindings/java/src/main/java/com/microsoft/regorus/Engine.java index 765c1de..3eabbe0 100644 --- a/bindings/java/src/main/java/com/microsoft/regorus/Engine.java +++ b/bindings/java/src/main/java/com/microsoft/regorus/Engine.java @@ -21,6 +21,7 @@ public class Engine implements AutoCloseable, Cloneable { // if you update the native API. private static native long nativeNewEngine(); private static native long nativeClone(long enginePtr); + private static native void nativePrepare(long enginePtr); private static native void nativeSetRegoV0(long enginePtr, boolean enable); private static native String nativeAddPolicy(long enginePtr, String path, String rego); private static native String nativeAddPolicyFromFile(long enginePtr, String path); @@ -65,6 +66,14 @@ public class Engine implements AutoCloseable, Cloneable { public Engine clone() { return new Engine(nativeClone(enginePtr)); } + + /** + * Prepares internal evaluation structures without executing a query. + * Optional: if skipped, first evaluation performs the same setup. + */ + public void prepare() { + nativePrepare(enginePtr); + } /** * Enable/disable Rego v0. diff --git a/bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java b/bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java index ae6f7dc..ac10917 100644 --- a/bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java +++ b/bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java @@ -22,6 +22,9 @@ public class EngineTest extends TestCase "package test\nmessage = concat(\", \", [input.message, data.message])" ); engine.addDataJson("{\"message\":\"World!\"}"); + engine.prepare(); + Engine template = engine.clone(); + template.close(); engine.setInputJson("{\"message\":\"Hello\"}"); resJson = engine.evalQuery("data.test.message"); } diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 9943c88..456df90 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -463,6 +463,13 @@ impl Engine { self.engine.take_prints() } + /// Prepare internal evaluation structures without executing a query. + /// + /// Optional: if skipped, first evaluation performs the same setup. + pub fn prepare(&mut self) -> Result<()> { + self.engine.prepare() + } + /// Clone a [`Engine`] /// /// To avoid having to parse same policy again, the engine can be cloned diff --git a/bindings/python/test.py b/bindings/python/test.py index 5ef1bef..55ecf2f 100644 --- a/bindings/python/test.py +++ b/bindings/python/test.py @@ -87,6 +87,7 @@ report = engine.get_coverage_report_pretty() print(report) # Clone engine +engine.prepare() engine1 = engine.clone() diff --git a/bindings/ruby/ext/regorusrb/src/lib.rs b/bindings/ruby/ext/regorusrb/src/lib.rs index c1746e4..544d52f 100644 --- a/bindings/ruby/ext/regorusrb/src/lib.rs +++ b/bindings/ruby/ext/regorusrb/src/lib.rs @@ -115,6 +115,13 @@ impl Engine { Ok(()) } + fn prepare(&self) -> Result<(), Error> { + self.engine + .borrow_mut() + .prepare() + .map_err(|e| Error::new(runtime_error(), format!("Failed to prepare engine: {e}"))) + } + fn get_packages(&self) -> Result, Error> { self.engine .borrow() @@ -373,6 +380,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { method!(Engine::add_data_from_json_file, 1), )?; engine_class.define_method("clear_data", method!(Engine::clear_data, 0))?; + engine_class.define_method("prepare", method!(Engine::prepare, 0))?; // input operations engine_class.define_method("set_input", method!(Engine::set_input, 1))?; diff --git a/bindings/ruby/test/test_regorus.rb b/bindings/ruby/test/test_regorus.rb index b3ea418..3ba1860 100644 --- a/bindings/ruby/test/test_regorus.rb +++ b/bindings/ruby/test/test_regorus.rb @@ -150,6 +150,7 @@ class TestRegorus < Minitest::Test end def test_engine_cloning + @engine.prepare cloned_engine = @engine.clone assert_instance_of ::Regorus::Engine, cloned_engine diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md index 6b08160..ea3caee 100644 --- a/bindings/wasm/README.md +++ b/bindings/wasm/README.md @@ -23,4 +23,7 @@ Run `cargo xtask build-wasm` to invoke wasm-pack with sensible defaults, or `car See [test.js](https://github.com/microsoft/regorus/blob/main/bindings/wasm/test.js) for example usage. For best performance with large policies, call `engine.prepare()` after loading -policy/data, then use `engine.clone()` to create per-request engines. +policy/data, then use `engine.clone()` to create per-request engines. If +`prepare()` is skipped, the first `eval*` call performs the same one-time +setup. Adding/changing policy or data after `prepare()` invalidates the +prepared state. diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 7b937b4..9db59c0 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -141,6 +141,9 @@ impl Engine { /// Clone this engine. /// /// Useful for creating per-request engines after loading policy/data once. + /// + /// Clone is designed to avoid reparsing policy text and reloading immutable + /// policy structures. Mutable evaluation state is copied for isolation. #[wasm_bindgen(js_name = "clone")] pub fn cloneEngine(&self) -> Engine { Clone::clone(self) @@ -168,8 +171,14 @@ impl Engine { /// Prepare the engine for evaluation. /// - /// This initializes internal evaluation structures so a cloned engine can - /// evaluate without requiring an initial "dummy" evaluation. + /// The first evaluation on an unprepared engine performs one-time setup. + /// Calling `prepare()` performs that setup eagerly. + /// + /// This is optional for correctness. If omitted, the first `eval*` call + /// implicitly performs preparation. + /// + /// If policies/data are modified after `prepare()`, preparation is + /// invalidated and must be performed again (explicitly or via first eval). pub fn prepare(&mut self) -> Result<(), JsValue> { self.engine.prepare().map_err(error_to_jsvalue) } diff --git a/src/engine.rs b/src/engine.rs index 831b3eb..4e4c1d5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -507,9 +507,22 @@ impl Engine { /// Prepare the engine for evaluation without executing a query or rule. /// - /// This parses and initializes internal evaluation data structures so that - /// subsequent evaluations (or cloned engines) can run without paying the - /// one-time preparation cost during the first evaluation call. + /// The first evaluation on an unprepared engine performs one-time setup + /// (analysis, scheduling, imports/rules processing, and initialization of + /// internal evaluation structures). Calling this method performs that work + /// eagerly so a later call to [`Engine::eval_rule`] / [`Engine::eval_query`] + /// does not pay that startup cost. + /// + /// This method is optional for correctness. If omitted, the first + /// evaluation will implicitly prepare the engine. + /// + /// Preparation is invalidated when policy/data that affects evaluation is + /// changed (for example: [`Engine::add_policy`], [`Engine::add_policy_from_file`], + /// [`Engine::add_data`], [`Engine::clear_data`]). In those cases, the next + /// evaluation (or another explicit call to `prepare`) performs setup again. + /// + /// This is especially useful before cloning template engines used for + /// repeated evaluations. /// /// ``` /// # use regorus::*;