Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,10 @@ $RECYCLE.BIN/
_NCrunch*

glide-logs/

# Test results and reports
reports/
testresults/

# Temporary submodules (not for commit)
Copy link
Collaborator

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?

StackExchange-Redis/
235 changes: 235 additions & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 convert_double_pointer_to_vec method in ffi.rs. Are we able to use that one or otherwise extract a helper?

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);
}
2 changes: 1 addition & 1 deletion sources/Valkey.Glide/Abstract/IDatabaseAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Valkey.Glide;
/// Describes functionality that is common to both standalone and cluster servers.<br />
/// See also <see cref="GlideClient" /> and <see cref="GlideClusterClient" />.
/// </summary>
public interface IDatabaseAsync : IConnectionManagementCommands, IGenericCommands, IGenericBaseCommands, IHashCommands, IHyperLogLogCommands, IListCommands, IServerManagementCommands, ISetCommands, ISortedSetCommands, IStringCommands
public interface IDatabaseAsync : IConnectionManagementCommands, IGenericCommands, IGenericBaseCommands, IHashCommands, IHyperLogLogCommands, IListCommands, IScriptingAndFunctionBaseCommands, IServerManagementCommands, ISetCommands, ISortedSetCommands, IStringCommands
{
/// <summary>
/// Execute an arbitrary command against the server; this is primarily intended for executing modules,
Expand Down
53 changes: 52 additions & 1 deletion sources/Valkey.Glide/Abstract/IServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,4 +249,55 @@ public interface IServer
/// </example>
/// </remarks>
Task<long> ClientIdAsync(CommandFlags flags = CommandFlags.None);
}

/// <summary>
/// Checks if a script exists in the server's script cache.
/// </summary>
/// <param name="script">The Lua script to check.</param>
/// <param name="flags">Command flags (currently not supported by GLIDE).</param>
/// <returns>A task representing the asynchronous operation, containing true if the script exists in the cache, false otherwise.</returns>
/// <remarks>
/// This method calculates the SHA1 hash of the script and checks if it exists in the server's cache.
/// </remarks>
Task<bool> ScriptExistsAsync(string script, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Checks if a script exists in the server's script cache by its SHA1 hash.
/// </summary>
/// <param name="sha1">The SHA1 hash of the script to check.</param>
/// <param name="flags">Command flags (currently not supported by GLIDE).</param>
/// <returns>A task representing the asynchronous operation, containing true if the script exists in the cache, false otherwise.</returns>
Task<bool> ScriptExistsAsync(byte[] sha1, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Loads a Lua script onto the server and returns its SHA1 hash.
/// </summary>
/// <param name="script">The Lua script to load.</param>
/// <param name="flags">Command flags (currently not supported by GLIDE).</param>
/// <returns>A task representing the asynchronous operation, containing the SHA1 hash of the loaded script.</returns>
/// <remarks>
/// The script is cached on the server and can be executed using EVALSHA with the returned hash.
/// </remarks>
Task<byte[]> ScriptLoadAsync(string script, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Loads a LuaScript onto the server and returns a LoadedLuaScript.
/// </summary>
/// <param name="script">The LuaScript to load.</param>
/// <param name="flags">Command flags (currently not supported by GLIDE).</param>
/// <returns>A task representing the asynchronous operation, containing a LoadedLuaScript instance.</returns>
/// <remarks>
/// The script is cached on the server and can be executed using the returned LoadedLuaScript.
/// </remarks>
Task<LoadedLuaScript> ScriptLoadAsync(LuaScript script, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Removes all scripts from the server's script cache.
/// </summary>
/// <param name="flags">Command flags (currently not supported by GLIDE).</param>
/// <returns>A task representing the asynchronous operation.</returns>
/// <remarks>
/// After calling this method, all scripts must be reloaded before they can be executed with EVALSHA.
/// </remarks>
Task ScriptFlushAsync(CommandFlags flags = CommandFlags.None);
} ///
79 changes: 79 additions & 0 deletions sources/Valkey.Glide/Abstract/ValkeyServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,83 @@ public async Task<string> LolwutAsync(CommandFlags flags = CommandFlags.None)
Utils.Requires<NotImplementedException>(flags == CommandFlags.None, "Command flags are not supported by GLIDE");
return await _conn.Command(Request.LolwutAsync(), MakeRoute());
}

public async Task<bool> ScriptExistsAsync(string script, CommandFlags flags = CommandFlags.None)
{
if (string.IsNullOrEmpty(script))
{
throw new ArgumentException("Script cannot be null or empty", nameof(script));
}

Utils.Requires<NotImplementedException>(flags == CommandFlags.None, "Command flags are not supported by GLIDE");

// Calculate SHA1 hash of the script
using Script scriptObj = new(script);
string hash = scriptObj.Hash;

// Call SCRIPT EXISTS with the hash
bool[] results = await _conn.Command(Request.ScriptExistsAsync([hash]), MakeRoute());
return results.Length > 0 && results[0];
}

public async Task<bool> ScriptExistsAsync(byte[] sha1, CommandFlags flags = CommandFlags.None)
{
if (sha1 == null || sha1.Length == 0)
{
throw new ArgumentException("SHA1 hash cannot be null or empty", nameof(sha1));
}

Utils.Requires<NotImplementedException>(flags == CommandFlags.None, "Command flags are not supported by GLIDE");

// Convert byte array to hex string
string hash = BitConverter.ToString(sha1).Replace("-", "").ToLowerInvariant();

// Call SCRIPT EXISTS with the hash
bool[] results = await _conn.Command(Request.ScriptExistsAsync([hash]), MakeRoute());
return results.Length > 0 && results[0];
}

public async Task<byte[]> ScriptLoadAsync(string script, CommandFlags flags = CommandFlags.None)
{
if (string.IsNullOrEmpty(script))
{
throw new ArgumentException("Script cannot be null or empty", nameof(script));
}

Utils.Requires<NotImplementedException>(flags == CommandFlags.None, "Command flags are not supported by GLIDE");

// Use custom command to call SCRIPT LOAD
ValkeyResult result = await ExecuteAsync("SCRIPT", ["LOAD", script], flags);
string? hashString = (string?)result;

if (string.IsNullOrEmpty(hashString))
{
throw new InvalidOperationException("SCRIPT LOAD returned null or empty hash");
}

// Convert hex string to byte array
return Convert.FromHexString(hashString);
}

public async Task<LoadedLuaScript> ScriptLoadAsync(LuaScript script, CommandFlags flags = CommandFlags.None)
{
if (script == null)
{
throw new ArgumentNullException(nameof(script));
}

Utils.Requires<NotImplementedException>(flags == CommandFlags.None, "Command flags are not supported by GLIDE");

// Load the executable script
byte[] hash = await ScriptLoadAsync(script.ExecutableScript, flags);
return new LoadedLuaScript(script, hash, script.ExecutableScript);
}

public async Task ScriptFlushAsync(CommandFlags flags = CommandFlags.None)
{
Utils.Requires<NotImplementedException>(flags == CommandFlags.None, "Command flags are not supported by GLIDE");

// Call SCRIPT FLUSH (default is SYNC mode)
_ = await _conn.Command(Request.ScriptFlushAsync(), MakeRoute());
}
}
Loading
Loading