-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Implement Lua Scripting and Functions Support (Issue #56) #120
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
Open
jbrinkman
wants to merge
13
commits into
main
Choose a base branch
from
jbrinkman/lua
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
25b5cd4
feat: Add FFI integration for script storage
jbrinkman 8e4c644
feat: Implement Script class with IDisposable pattern
jbrinkman 3585a1a
feat(scripting): implement ScriptParameterMapper utility
jbrinkman 938e84f
feat(scripting): implement LuaScript class for StackExchange.Redis co…
jbrinkman 486af8f
feat: Implement ScriptOptions and ClusterScriptOptions
jbrinkman a2e6acd
feat: implement function data models
jbrinkman 82d6398
feat(scripting): define command interfaces for scripting and functions
jbrinkman 425f0c2
feat(functions): implement standalone-specific function commands
jbrinkman a94be99
feat(scripting): implement base scripting and function commands
jbrinkman db2de2c
feat(cluster): implement cluster-specific script commands and fix Fun…
jbrinkman 1e220fb
fix: temporarily skip multi-database cluster tests
jbrinkman 6e32ec1
feat: Add cluster routing support for function commands
jbrinkman 2844a51
fix: resolve LoadedLuaScript execution and test suite hanging issues
jbrinkman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -438,3 +438,238 @@ pub unsafe extern "C" fn init(level: Option<Level>, file_name: *const c_char) -> | |
| let logger_level = logger_core::init(level.map(|level| level.into()), file_name_as_str); | ||
| logger_level.into() | ||
| } | ||
|
|
||
| #[repr(C)] | ||
| pub struct ScriptHashBuffer { | ||
| pub ptr: *mut u8, | ||
| pub len: usize, | ||
| pub capacity: usize, | ||
| } | ||
|
|
||
| /// Store a Lua script in the script cache and return its SHA1 hash. | ||
| /// | ||
| /// # Parameters | ||
| /// | ||
| /// * `script_bytes`: Pointer to the script bytes. | ||
| /// * `script_len`: Length of the script in bytes. | ||
| /// | ||
| /// # Returns | ||
| /// | ||
| /// A pointer to a `ScriptHashBuffer` containing the SHA1 hash of the script. | ||
| /// The caller is responsible for freeing this memory using [`free_script_hash_buffer`]. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// * `script_bytes` must point to `script_len` consecutive properly initialized bytes. | ||
| /// * The returned buffer must be freed by the caller using [`free_script_hash_buffer`]. | ||
| #[unsafe(no_mangle)] | ||
| pub unsafe extern "C" fn store_script( | ||
| script_bytes: *const u8, | ||
| script_len: usize, | ||
| ) -> *mut ScriptHashBuffer { | ||
| let script = unsafe { std::slice::from_raw_parts(script_bytes, script_len) }; | ||
| let hash = glide_core::scripts_container::add_script(script); | ||
| let mut hash = std::mem::ManuallyDrop::new(hash); | ||
| let script_hash_buffer = ScriptHashBuffer { | ||
| ptr: hash.as_mut_ptr(), | ||
| len: hash.len(), | ||
| capacity: hash.capacity(), | ||
| }; | ||
| Box::into_raw(Box::new(script_hash_buffer)) | ||
| } | ||
|
|
||
| /// Free a `ScriptHashBuffer` obtained from [`store_script`]. | ||
| /// | ||
| /// # Parameters | ||
| /// | ||
| /// * `buffer`: Pointer to the `ScriptHashBuffer`. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// * `buffer` must be a pointer returned from [`store_script`]. | ||
| /// * This function must be called exactly once per buffer. | ||
| #[unsafe(no_mangle)] | ||
| pub unsafe extern "C" fn free_script_hash_buffer(buffer: *mut ScriptHashBuffer) { | ||
| if buffer.is_null() { | ||
| return; | ||
| } | ||
| let buffer = unsafe { Box::from_raw(buffer) }; | ||
| let _hash = unsafe { String::from_raw_parts(buffer.ptr, buffer.len, buffer.capacity) }; | ||
| } | ||
|
|
||
| /// Remove a script from the script cache. | ||
| /// | ||
| /// Returns a null pointer if it succeeds and a C string error message if it fails. | ||
| /// | ||
| /// # Parameters | ||
| /// | ||
| /// * `hash`: The SHA1 hash of the script to remove as a byte array. | ||
| /// * `len`: The length of `hash`. | ||
| /// | ||
| /// # Returns | ||
| /// | ||
| /// A null pointer on success, or a pointer to a C string error message on failure. | ||
| /// The caller is responsible for freeing the error message using [`free_drop_script_error`]. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// * `hash` must be a valid pointer to a UTF-8 string. | ||
| /// * The returned error pointer (if not null) must be freed using [`free_drop_script_error`]. | ||
| #[unsafe(no_mangle)] | ||
| pub unsafe extern "C" fn drop_script(hash: *mut u8, len: usize) -> *mut c_char { | ||
| if hash.is_null() { | ||
| return CString::new("Hash pointer was null.").unwrap().into_raw(); | ||
| } | ||
|
|
||
| let slice = std::ptr::slice_from_raw_parts_mut(hash, len); | ||
| let Ok(hash_str) = std::str::from_utf8(unsafe { &*slice }) else { | ||
| return CString::new("Unable to convert hash to UTF-8 string.") | ||
| .unwrap() | ||
| .into_raw(); | ||
| }; | ||
|
|
||
| glide_core::scripts_container::remove_script(hash_str); | ||
| std::ptr::null_mut() | ||
| } | ||
|
|
||
| /// Free an error message from a failed drop_script call. | ||
| /// | ||
| /// # Parameters | ||
| /// | ||
| /// * `error`: The error to free. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// * `error` must be an error returned by [`drop_script`]. | ||
| /// * This function must be called exactly once per error. | ||
| #[unsafe(no_mangle)] | ||
| pub unsafe extern "C" fn free_drop_script_error(error: *mut c_char) { | ||
| if !error.is_null() { | ||
| _ = unsafe { CString::from_raw(error) }; | ||
| } | ||
| } | ||
|
|
||
| /// Executes a Lua script using EVALSHA with automatic fallback to EVAL. | ||
| /// | ||
| /// # Parameters | ||
| /// | ||
| /// * `client_ptr`: Pointer to a valid `GlideClient` returned from [`create_client`]. | ||
| /// * `callback_index`: Unique identifier for the callback. | ||
| /// * `hash`: SHA1 hash of the script as a null-terminated C string. | ||
| /// * `keys_count`: Number of keys in the keys array. | ||
| /// * `keys`: Array of pointers to key data. | ||
| /// * `keys_len`: Array of key lengths. | ||
| /// * `args_count`: Number of arguments in the args array. | ||
| /// * `args`: Array of pointers to argument data. | ||
| /// * `args_len`: Array of argument lengths. | ||
| /// * `route_bytes`: Optional routing information (not used, reserved for future). | ||
| /// * `route_bytes_len`: Length of route_bytes. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// * `client_ptr` must not be `null` and must be obtained from [`create_client`]. | ||
| /// * `hash` must be a valid null-terminated C string. | ||
| /// * `keys` and `keys_len` must be valid arrays of size `keys_count`, or both null if `keys_count` is 0. | ||
| /// * `args` and `args_len` must be valid arrays of size `args_count`, or both null if `args_count` is 0. | ||
| #[unsafe(no_mangle)] | ||
| pub unsafe extern "C-unwind" fn invoke_script( | ||
| client_ptr: *const c_void, | ||
| callback_index: usize, | ||
| hash: *const c_char, | ||
| keys_count: usize, | ||
| keys: *const usize, | ||
| keys_len: *const usize, | ||
| args_count: usize, | ||
| args: *const usize, | ||
| args_len: *const usize, | ||
| _route_bytes: *const u8, | ||
| _route_bytes_len: usize, | ||
| ) { | ||
| let client = unsafe { | ||
| Arc::increment_strong_count(client_ptr); | ||
| Arc::from_raw(client_ptr as *mut Client) | ||
| }; | ||
| let core = client.core.clone(); | ||
|
|
||
| let mut panic_guard = PanicGuard { | ||
| panicked: true, | ||
| failure_callback: core.failure_callback, | ||
| callback_index, | ||
| }; | ||
|
|
||
| // Convert hash to Rust string | ||
| let hash_str = match unsafe { CStr::from_ptr(hash).to_str() } { | ||
| Ok(s) => s.to_string(), | ||
| Err(e) => { | ||
| unsafe { | ||
| report_error( | ||
| core.failure_callback, | ||
| callback_index, | ||
| format!("Invalid hash string: {}", e), | ||
| RequestErrorType::Unspecified, | ||
| ); | ||
| } | ||
| return; | ||
| } | ||
| }; | ||
|
|
||
| // Convert keys | ||
| let keys_vec: Vec<&[u8]> = if !keys.is_null() && !keys_len.is_null() && keys_count > 0 { | ||
|
Comment on lines
+616
to
+617
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block (as well as the one below) seems to be similar to the existing |
||
| let key_ptrs = unsafe { std::slice::from_raw_parts(keys as *const *const u8, keys_count) }; | ||
| let key_lens = unsafe { std::slice::from_raw_parts(keys_len, keys_count) }; | ||
| key_ptrs | ||
| .iter() | ||
| .zip(key_lens.iter()) | ||
| .map(|(&ptr, &len)| unsafe { std::slice::from_raw_parts(ptr, len) }) | ||
| .collect() | ||
| } else { | ||
| Vec::new() | ||
| }; | ||
|
|
||
| // Convert args | ||
| let args_vec: Vec<&[u8]> = if !args.is_null() && !args_len.is_null() && args_count > 0 { | ||
| let arg_ptrs = unsafe { std::slice::from_raw_parts(args as *const *const u8, args_count) }; | ||
| let arg_lens = unsafe { std::slice::from_raw_parts(args_len, args_count) }; | ||
| arg_ptrs | ||
| .iter() | ||
| .zip(arg_lens.iter()) | ||
| .map(|(&ptr, &len)| unsafe { std::slice::from_raw_parts(ptr, len) }) | ||
| .collect() | ||
| } else { | ||
| Vec::new() | ||
| }; | ||
|
|
||
| client.runtime.spawn(async move { | ||
| let mut panic_guard = PanicGuard { | ||
| panicked: true, | ||
| failure_callback: core.failure_callback, | ||
| callback_index, | ||
| }; | ||
|
|
||
| let result = core | ||
| .client | ||
| .clone() | ||
| .invoke_script(&hash_str, &keys_vec, &args_vec, None) | ||
| .await; | ||
|
|
||
| match result { | ||
| Ok(value) => { | ||
| let ptr = Box::into_raw(Box::new(ResponseValue::from_value(value))); | ||
| unsafe { (core.success_callback)(callback_index, ptr) }; | ||
| } | ||
| Err(err) => unsafe { | ||
| report_error( | ||
| core.failure_callback, | ||
| callback_index, | ||
| error_message(&err), | ||
| error_type(&err), | ||
| ); | ||
| }, | ||
| }; | ||
| panic_guard.panicked = false; | ||
| drop(panic_guard); | ||
| }); | ||
|
|
||
| panic_guard.panicked = false; | ||
| drop(panic_guard); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious why this is needed?