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
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ crate-type = ["cdylib"]
name = "string"
crate-type = ["cdylib"]

[[example]]
name = "borrowed_strings"
crate-type = ["cdylib"]

[[example]]
name = "configuration"
crate-type = ["cdylib"]
Expand Down
45 changes: 45 additions & 0 deletions examples/borrowed_strings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use valkey_module::alloc::ValkeyAlloc;
use valkey_module::{valkey_module, Context, ValkeyError, ValkeyResult, ValkeyString, ValkeyValue};

/// Concatenates the raw bytes of all arguments after the command name and
/// echoes them back via [`Context::reply_with_valkey_string`], using
/// [`Context::create_string_from_slice`] to materialize the [`ValkeyString`]
/// without going through `CString::new`. Round-trips bytes containing NUL.
fn echo_via_string(ctx: &Context, args: Vec<ValkeyString>) -> ValkeyResult {
if args.len() < 2 {
return Err(ValkeyError::WrongArity);
}
let mut buf = Vec::new();
for arg in args.iter().skip(1) {
buf.extend_from_slice(arg.as_slice());
}
let s = ctx.create_string_from_slice(&buf);
ctx.reply_with_valkey_string(&s);
Ok(ValkeyValue::NoReply)
}

/// Concatenates the raw bytes of all arguments after the command name and
/// echoes them back via [`Context::reply_with_slice`], without ever
/// constructing a [`ValkeyString`] or [`ValkeyValue`].
fn echo_via_slice(ctx: &Context, args: Vec<ValkeyString>) -> ValkeyResult {
if args.len() < 2 {
return Err(ValkeyError::WrongArity);
}
let mut buf = Vec::new();
for arg in args.iter().skip(1) {
buf.extend_from_slice(arg.as_slice());
}
ctx.reply_with_slice(&buf);
Ok(ValkeyValue::NoReply)
}

valkey_module! {
name: "borrowed_strings",
version: 1,
allocator: (ValkeyAlloc, ValkeyAlloc),
data_types: [],
commands: [
["borrowed_strings.echo_string", echo_via_string, "readonly fast", 0, 0, 0],
["borrowed_strings.echo_slice", echo_via_slice, "readonly fast", 0, 0, 0],
],
}
34 changes: 34 additions & 0 deletions src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,28 @@ impl Context {
unsafe { raw::RedisModule_ReplyWithError.unwrap()(self.ctx, msg.as_ptr()).into() }
}

/// Reply with a borrowed byte slice, without copying into a [`ValkeyValue`].
///
/// Use this on hot paths where you already hold a `&[u8]` (for example a
/// slice borrowed from an open key) and do not want to allocate a `Vec`
/// just to construct [`ValkeyValue::StringBuffer`]. Wraps
/// [`ValkeyModule_ReplyWithStringBuffer`](https://valkey.io/topics/modules-api-ref/#ValkeyModule_ReplyWithStringBuffer).
#[allow(clippy::must_use_candidate)]
pub fn reply_with_slice(&self, s: &[u8]) -> raw::Status {
raw::reply_with_string_buffer(self.ctx, s.as_ptr().cast::<c_char>(), s.len())
}

/// Reply with a borrowed [`ValkeyString`], without cloning it.
///
/// [`Self::reply`] with [`ValkeyValue::BulkValkeyString`] requires owning
/// the `ValkeyString`. When the value is borrowed (for example from an
/// open key) this method hands the pointer straight to the C API. Wraps
/// [`ValkeyModule_ReplyWithString`](https://valkey.io/topics/modules-api-ref/#ValkeyModule_ReplyWithString).
#[allow(clippy::must_use_candidate)]
pub fn reply_with_valkey_string(&self, s: &ValkeyString) -> raw::Status {
raw::reply_with_string(self.ctx, s.inner)
}

#[cfg(feature = "min-valkey-compatibility-version-8-0")]
pub fn add_acl_category(&self, s: &str) -> raw::Status {
let acl_flags = Self::str_as_legal_resp_string(s);
Expand Down Expand Up @@ -680,6 +702,18 @@ impl Context {
ValkeyString::create(NonNull::new(self.ctx), s)
}

/// Binary-safe variant of [`Self::create_string`].
///
/// [`Self::create_string`] routes its input through `CString::new`, which
/// allocates and panics on inputs containing a NUL byte. This method
/// hands the slice straight to
/// [`ValkeyModule_CreateString`](https://valkey.io/topics/modules-api-ref/#ValkeyModule_CreateString),
/// so any byte sequence is accepted unchanged.
#[must_use]
pub fn create_string_from_slice(&self, s: &[u8]) -> ValkeyString {
ValkeyString::create_from_slice(self.ctx, s)
}

#[must_use]
pub const fn get_raw(&self) -> *mut raw::RedisModuleCtx {
self.ctx
Expand Down
27 changes: 27 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1993,3 +1993,30 @@ fn test_swapdb_event() -> Result<()> {

Ok(())
}

#[test]
fn test_borrowed_strings() -> Result<()> {
let port: u16 = 6529;
let _guards = vec![start_valkey_server_with_module("borrowed_strings", port)
.with_context(|| FAILED_TO_START_SERVER)?];
let mut con = get_valkey_connection(port).with_context(|| FAILED_TO_CONNECT_TO_SERVER)?;

// Bytes containing embedded NULs would panic if routed through
// `Context::create_string`'s `CString::new`, but the `_from_slice`
// variant accepts arbitrary bytes.
let with_nul: &[u8] = &[b'a', 0, b'b', 0, b'c'];

let res: Vec<u8> = redis::cmd("borrowed_strings.echo_string")
.arg(with_nul)
.arg(&[0u8, 0, 0][..])
.query(&mut con)?;
assert_eq!(res, [b'a', 0, b'b', 0, b'c', 0, 0, 0]);

let res: Vec<u8> = redis::cmd("borrowed_strings.echo_slice")
.arg(with_nul)
.arg(&[0u8, 0, 0][..])
.query(&mut con)?;
assert_eq!(res, [b'a', 0, b'b', 0, b'c', 0, 0, 0]);

Ok(())
}