-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Persist the state with vm.invoke #4175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
First of all, this is not related to your question directly, but how did you initialize the scope? If you have a python module like the upper one, it will be initialized like: let module = vm.import(<path>, None, 0); and then let record_message = module.get_attr("record_message", vm);
... Then If you are going to access a low-level interface, you can create code object instead of a module. let code_obj = vm.compile(<path>); // or compile_string with source string Then you can use |
I will post code later, but to initialize the interpreter I just use run_code_string with all of the user's code. Hmm, this is troubling for my use case. I need to set up the user's code once when the canister is initialized. A canister is like an everlasting server process, it is a smart contract on the Internet Computer that has its own dedicated Wasm memory. Users interact with a canister by performing query and update calls over http. An update call is basically a function call that persists state. I need to be able to call a Python function during an update call, pass in the arguments that the user has provided, get a result, and have whatever state changes occured during the Python function call persisted. I'm not seeing how I can practically achieve this by passing arguments into invoke, nor by using run_code_obj. For Azle, our TypeScript/JavaScript environment for the IC, the JS engine we use Boa simply automatically persists its state. Once you create a VM instance (known as a context), all of the state is in there. If you grab a function and call it, all state changes will remain in the memory of the VM. Can I not achieve this with RustPython? This is a MAJOR blocker at this point. Hopefully I'm missing something. |
Here's a more complete program after all of our compilation (I've left out a couple traits and impls to save on verbosity). This will run on the Internet Computer inside of a canister. The comments explain the situation: // This file represents an entire Rust canister. It will be compiled to wasm32-unknown-unknown and deployed to an Internet Computer replica.
// On first deploy, _kybra_init is executed. This initializes the developer's code
// On subsequent deploys, _kybra_post_upgrade is executed
// Between deploys, the canister is an everlasting process. Its memory is automatically persisted by the system
// Users interact with the canister by calling into the query and update methods
// query methods do not persist state
// update methods MUST persist state
use rustpython;
use rustpython::vm::convert::ToPyObject;
// We store the interpreter and scope here so that we can access them during each query or update call
// Ideally we could just store the interpreter and it would simply maintain state, this is how the JS engine Boa works for Azle
static mut _KYBRA_INTERPRETER_OPTION: Option<rustpython::vm::Interpreter> = None;
static mut _KYBRA_SCOPE_OPTION: Option<rustpython::vm::scope::Scope> = None;
// MAIN_PY has the user's code. We take it from the filesystem during the Kybra compilation process
static MAIN_PY : & 'static str = "# TODO should go in an azle.py file that can be imported\n# from typing import Any # TODO importing Any seems to break in many cases (maybe all cases?)\n\nint64 = int\nint32 = int\nint16 = int\nint8 = int\n\nnat = int\nnat64 = int\nnat32 = int\nnat16 = int\nnat8 = int\n\nfloat32 = float\nfloat64 = float\n\ntext = str\n\n# blob = bytes\n\n# reserved = Any\n# empty = Any\n\ndef query(func: object):\n return func\n\ndef update(func: object):\n return func\n# TODO should go in an azle.py file that can be imported\n\n@query\ndef concat(x: str, y: str) -> str:\n return x + y\n\n@query\ndef add(x: int, y: int) -> int:\n return x + y\n\nsaved_message = ''\n\n@update\ndef record_message(message: str) -> str:\n saved_message = message\n return saved_message\n\n@query\ndef get_message() -> str:\n return saved_message\n" ;
// This only executes when the canister is first installed
// We setup all of the user's code with a simple vm.run_code_string
#[ic_cdk_macros::init]
fn _kybra_init() {
unsafe {
let _kybra_interpreter = rustpython::vm::Interpreter::without_stdlib(Default::default());
let _kybra_scope = _kybra_interpreter.enter(|vm| vm.new_scope_with_builtins());
_kybra_interpreter.enter(|vm| {
vm.run_code_string(_kybra_scope.clone(), MAIN_PY, "".to_owned())
.unwrap();
});
_KYBRA_INTERPRETER_OPTION = Some(_kybra_interpreter);
_KYBRA_SCOPE_OPTION = Some(_kybra_scope);
}
}
// This executes when the canister is subsequently upgraded
// We setup all of the user's code with a simple vm.run_code_string
#[ic_cdk_macros::post_upgrade]
fn _kybra_post_upgrade() {
unsafe {
let _kybra_interpreter = rustpython::vm::Interpreter::without_stdlib(Default::default());
let _kybra_scope = _kybra_interpreter.enter(|vm| vm.new_scope_with_builtins());
_kybra_interpreter.enter(|vm| {
vm.run_code_string(_kybra_scope.clone(), MAIN_PY, "".to_owned())
.unwrap();
});
_KYBRA_INTERPRETER_OPTION = Some(_kybra_interpreter);
_KYBRA_SCOPE_OPTION = Some(_kybra_scope);
}
}
fn custom_getrandom(_buf: &mut [u8]) -> Result<(), getrandom::Error> {
Ok(())
}
getrandom::register_custom_getrandom!(custom_getrandom);
// Query methods do not persist state, but they expect the state to be updated from the last update call
#[ic_cdk_macros::query]
#[candid::candid_method(query)]
async fn concat(x: String, y: String) -> String {
unsafe {
let _kybra_interpreter = _KYBRA_INTERPRETER_OPTION.as_mut().unwrap();
let _kybra_scope = _KYBRA_SCOPE_OPTION.as_mut().unwrap();
let result = _kybra_interpreter.enter(|vm| {
let method_py_object_ref = _kybra_scope.globals.get_item("concat", vm).unwrap();
let result_py_object_ref = vm
.invoke(
&method_py_object_ref,
(
x.try_into_vm_value(vm).unwrap(),
y.try_into_vm_value(vm).unwrap(),
),
)
.unwrap();
result_py_object_ref.try_from_vm_value(vm).unwrap()
});
result
}
}
// Query methods do not persist state, but they expect the state to be updated from the last update call
#[ic_cdk_macros::query]
#[candid::candid_method(query)]
async fn add(x: i128, y: i128) -> i128 {
unsafe {
let _kybra_interpreter = _KYBRA_INTERPRETER_OPTION.as_mut().unwrap();
let _kybra_scope = _KYBRA_SCOPE_OPTION.as_mut().unwrap();
let result = _kybra_interpreter.enter(|vm| {
let method_py_object_ref = _kybra_scope.globals.get_item("add", vm).unwrap();
let result_py_object_ref = vm
.invoke(
&method_py_object_ref,
(
x.try_into_vm_value(vm).unwrap(),
y.try_into_vm_value(vm).unwrap(),
),
)
.unwrap();
result_py_object_ref.try_from_vm_value(vm).unwrap()
});
result
}
}
// Update methods MUST persist state
// Notice that we need to take all of the user-provided arguments and pass them in as function arguments to the Python function
// Notice that the Rust code is effectively acting as a proxy to the Python code
#[ic_cdk_macros::update]
#[candid::candid_method(update)]
async fn record_message(message: String) -> String {
unsafe {
let _kybra_interpreter = _KYBRA_INTERPRETER_OPTION.as_mut().unwrap();
let _kybra_scope = _KYBRA_SCOPE_OPTION.as_mut().unwrap();
let result = _kybra_interpreter.enter(|vm| {
let method_py_object_ref = _kybra_scope.globals.get_item("record_message", vm).unwrap();
let result_py_object_ref = vm
.invoke(
&method_py_object_ref,
(message.try_into_vm_value(vm).unwrap(),),
)
.unwrap();
result_py_object_ref.try_from_vm_value(vm).unwrap()
});
result
}
}
#[ic_cdk_macros::query]
#[candid::candid_method(query)]
async fn get_message() -> String {
unsafe {
let _kybra_interpreter = _KYBRA_INTERPRETER_OPTION.as_mut().unwrap();
let _kybra_scope = _KYBRA_SCOPE_OPTION.as_mut().unwrap();
let result = _kybra_interpreter.enter(|vm| {
let method_py_object_ref = _kybra_scope.globals.get_item("get_message", vm).unwrap();
let result_py_object_ref = vm.invoke(&method_py_object_ref, ()).unwrap();
result_py_object_ref.try_from_vm_value(vm).unwrap()
});
result
}
}
candid::export_service!();
#[ic_cdk_macros::query(name = "__get_candid_interface_tmp_hack")]
fn export_candid() -> String {
__export_service()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_candid_to_disk() {
std::fs::write("main.did", export_candid()).unwrap();
}
} |
As a first step I'm just trying to get this to work within a single interpreter.enter not using scope: message = 'it did not work'
def set():
message = 'it did work'
return message
def get():
return message Here's the Rust code: fn main() {
let interpreter = rustpython::vm::Interpreter::with_init(Default::default(), |vm| {
vm.add_frozen(rustpython::vm::py_freeze!(
file = "src/main.py",
module_name = "user_code"
));
});
interpreter.enter(|vm| {
let module_result = vm.invoke(&vm.import_func, ("user_code",)).unwrap();
let set = module_result.get_attr("set", vm).unwrap();
vm.invoke(&set, ()).unwrap();
let get = module_result.get_attr("get", vm).unwrap();
let get_result = vm.invoke(&get, ()).unwrap();
let get_result_string: String = get_result.try_into_value(vm).unwrap();
println!("get_result_string: {}", get_result_string);
});
} I've loaded the main.py as a frozen module. |
Okay so this is really dumb, but I just needed to use the I would love some suggestions on how to possibly do this better though, my next steps are to compile or freeze my modules and I'm hoping it will transfer over well enough. |
Is there a way to persist the state/scope when using vm.invoke? For my application, I have a global interpreter that is executed once with the user's code. This sets up their Python application. This is running on the Internet Computer, basically a decentralized cloud environment. The interpreter will always be running, such as in a long-running server process.
I need the interpreter and the scope to be persisted across calls. Right now
record_message
does not persist the message. I'm not sure how to provide the scope appropriately when usingvm.invoke
.The text was updated successfully, but these errors were encountered: