Skip to content

feat(eventlistener) support eventlistener interface#1

Open
ruojieranyishen wants to merge 4 commits intoarana-db:masterfrom
ruojieranyishen:addeventlistener
Open

feat(eventlistener) support eventlistener interface#1
ruojieranyishen wants to merge 4 commits intoarana-db:masterfrom
ruojieranyishen:addeventlistener

Conversation

@ruojieranyishen
Copy link
Collaborator

@ruojieranyishen ruojieranyishen commented Dec 13, 2025

Summary by CodeRabbit

  • New Features

    • Added event-listener support for RocksDB, enabling registration of custom listeners to receive flush lifecycle notifications and flush-job metadata (column family, file path, sequence numbers).
    • Exposed safe listener handle and flush-info types for building and managing listeners from user code.
  • Tests

    • Added test validating that flush-begin and flush-completed callbacks are invoked.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 13, 2025

Walkthrough

Adds Rust-side RocksDB event listener support: new event_listener module (FlushJobInfo, EventListener trait, EventListenerHandle, new_event_listener), a method on Options to attach a listener, crate re-exports, and an integration test exercising flush callbacks.

Changes

Cohort / File(s) Summary
Core event listener implementation
src/event_listener.rs
New module providing FlushJobInfo (safe accessors), EventListener trait (on_flush_begin / on_flush_completed), FFI callbacks/destructor, new_event_listener<E: EventListener>(e: E) that boxes the listener and returns an EventListenerHandle, and EventListenerHandle type with ownership / Drop semantics.
Options integration
src/db_options.rs
Adds pub fn add_event_listener(&mut self, listener: EventListenerHandle) which converts the handle into a raw pointer and calls ffi::rocksdb_options_add_eventlistener(self.inner, ptr) inside unsafe.
Public API exposure
src/lib.rs
Adds mod event_listener; and re-exports new_event_listener, EventListener, EventListenerHandle, and FlushJobInfo from the crate root.
Event listener testing
tests/test_event_listener.rs
New integration test test_flush_callback with TestEventListener using atomics; registers a listener, triggers a flush, and asserts on_flush_begin and on_flush_completed were each invoked once.

Sequence Diagram

sequenceDiagram
    participant User as User code
    participant EL as new_event_listener (Rust)
    participant Handle as EventListenerHandle
    participant Opts as Options
    participant FFI as RocksDB FFI
    participant DB as RocksDB

    User->>EL: call new_event_listener(listener)
    EL->>EL: box listener, build C callbacks & destructor
    EL->>Handle: return EventListenerHandle
    User->>Opts: add_event_listener(handle)
    Opts->>FFI: rocksdb_options_add_eventlistener(opts, raw_ptr)
    User->>DB: open DB with Options (listener attached)
    Note over DB,FFI: later, a flush occurs
    FFI->>FFI: create flush_job_info
    FFI->>Handle: call on_flush_begin callback(ctx, flush_job_info)
    Handle->>EL: translate pointers -> FlushJobInfo
    EL->>User: invoke EventListener::on_flush_begin(&FlushJobInfo)
    FFI->>Handle: call on_flush_completed callback(ctx, flush_job_info)
    EL->>User: invoke EventListener::on_flush_completed(&FlushJobInfo)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect unsafe FFI callbacks and destructor for correct pointer casts and panic-safety.
  • Verify FlushJobInfo C-string conversions and numeric field reads.
  • Confirm EventListenerHandle ownership semantics, Drop implementation, and Send/Sync assertions.
  • Check Options::add_event_listener signature and raw-pointer conversion match FFI expectation.
  • Review test synchronization (atomic counters) and DB setup/teardown in tests/test_event_listener.rs.

Poem

🐰 I boxed a listener, ears up and bright,
Hopped through C callbacks late at night,
Flush bells rang, two counters hum,
Rust and RocksDB danced — here I come! 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(eventlistener) support eventlistener interface' accurately describes the main change: adding EventListener interface support to the RocksDB Rust bindings with complete implementations across db_options.rs, event_listener.rs, lib.rs, and tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/db_options.rs (1)

1853-1880: API signature exposes raw pointer - consider a safer wrapper.

The public API takes a raw *mut ffi::rocksdb_eventlistener_t pointer, which is unusual compared to other methods in this file. The docstring example also shows a different API (EventListener::new(...)) than what's actually implemented.

Consider either:

  1. Accept the EventListener trait object directly and call new_event_listener internally
  2. Or document that users must call new_event_listener() first

Additionally, other similar registrations (e.g., set_env, set_row_cache) store references in outlive to ensure proper lifetime management. The event listener may need similar treatment to prevent use-after-free if the listener is dropped before the DB.

-    pub fn add_event_listener(&mut self, listener: *mut ffi::rocksdb_eventlistener_t) {
+    pub fn add_event_listener<E: EventListener + 'static>(&mut self, listener: E) {
+        let listener_ptr = crate::new_event_listener(listener);
         unsafe {
-            ffi::rocksdb_options_add_eventlistener(self.inner, listener);
+            ffi::rocksdb_options_add_eventlistener(self.inner, listener_ptr);
         }
     }
src/event_listener.rs (2)

26-28: FlushJobInfo holds a raw pointer with unclear lifetime.

The FlushJobInfo struct holds a raw pointer to ffi::rocksdb_flushjobinfo_t which is only valid during the callback execution. If a user stores the FlushJobInfo reference or clones data incorrectly, it could lead to use-after-free.

Consider documenting this limitation or making FlushJobInfo non-copyable/non-cloneable explicitly, or using a lifetime parameter to tie it to the callback scope.

+/// Wrapper around flush job information from RocksDB.
+/// 
+/// # Safety
+/// This struct is only valid during the callback invocation. 
+/// Do not store references to this struct beyond the callback scope.
 pub struct FlushJobInfo {
     pub(crate) inner: *const ffi::rocksdb_flushjobinfo_t,
 }

177-195: Document ownership semantics for new_event_listener.

The function returns a raw pointer where ownership is implicitly transferred. Consider adding documentation to clarify that:

  1. The returned pointer should be passed to Options::add_event_listener
  2. RocksDB takes ownership and will call the destructor when appropriate
  3. The caller should not manually free this pointer
+/// Creates a new RocksDB event listener from a Rust implementation.
+///
+/// # Ownership
+/// The returned pointer is owned by the caller and should be passed to
+/// `Options::add_event_listener`. RocksDB will take ownership and
+/// automatically destroy the listener when the database is closed.
+/// Do not manually free the returned pointer.
 pub fn new_event_listener<E: EventListener>(e: E) -> *mut ffi::rocksdb_eventlistener_t {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3f6cb5 and 153e294.

📒 Files selected for processing (4)
  • src/db_options.rs (1 hunks)
  • src/event_listener.rs (1 hunks)
  • src/lib.rs (2 hunks)
  • tests/test_event_listener.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/event_listener.rs (2)
src/ffi_util.rs (1)
  • from_raw_parts (219-221)
tests/test_event_listener.rs (1)
  • on_flush_begin (28-31)
src/lib.rs (1)
src/event_listener.rs (1)
  • new_event_listener (177-195)
tests/test_event_listener.rs (1)
src/event_listener.rs (8)
  • new_event_listener (177-195)
  • on_flush_begin (75-75)
  • on_flush_begin (91-101)
  • on_flush_begin (183-183)
  • file_path (40-47)
  • on_flush_completed (76-76)
  • on_flush_completed (103-113)
  • on_flush_completed (184-184)
🔇 Additional comments (3)
src/event_listener.rs (1)

87-89: LGTM!

The destructor correctly reconstructs the Box from the raw pointer to properly drop the listener.

src/lib.rs (1)

97-97: LGTM!

The module declaration and public re-exports follow the existing patterns in the crate and properly expose the new event listener API.

Also applies to: 135-135

tests/test_event_listener.rs (1)

38-63: LGTM!

The test properly exercises the event listener functionality by:

  • Creating a listener with shared atomic counters
  • Registering it with the DB options
  • Triggering a flush and verifying both callbacks are invoked

Consider adding a test case that verifies behavior with multiple flushes or edge cases in the future.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/event_listener.rs (1)

94-116: Callbacks should catch panics to prevent undefined behavior.

As noted in the comment at lines 74-76 and previously flagged, if the user's EventListener implementation panics, the panic will unwind through the C/FFI boundary, which is undefined behavior in Rust. The code still lacks panic guards.

🧹 Nitpick comments (2)
src/event_listener.rs (2)

118-176: Consider passing NULL for unimplemented callbacks or add FIXME markers.

These stub functions are registered but perform no actions. Since the corresponding trait methods are commented out (lines 80-87), users cannot implement them. Registering no-op callbacks could be misleading.

Options:

  1. Pass None instead of Some(stub_fn) in new_event_listener for unimplemented callbacks
  2. Add FIXME or HACK comments to indicate these are placeholders awaiting proper implementation
🔎 Option 1: Pass None for unimplemented callbacks
 pub fn new_event_listener<E: EventListener>(e: E) -> *mut ffi::rocksdb_eventlistener_t {
     let p = Box::new(e);
     unsafe {
         ffi::rocksdb_eventlistener_create(
             Box::into_raw(p) as *mut c_void,
             Some(destructor::<E>),
             Some(on_flush_begin::<E>),
             Some(on_flush_completed::<E>),
-            Some(on_compaction_begin::<E>),
-            Some(on_compaction_completed::<E>),
-            Some(on_subcompaction_begin::<E>),
-            Some(on_subcompaction_completed::<E>),
-            Some(on_external_file_ingested::<E>),
-            Some(on_background_error::<E>),
-            Some(on_stall_conditions_changed::<E>),
-            Some(on_memtable_sealed::<E>),
+            None, // on_compaction_begin - TODO
+            None, // on_compaction_completed - TODO
+            None, // on_subcompaction_begin - TODO
+            None, // on_subcompaction_completed - TODO
+            None, // on_external_file_ingested - TODO
+            None, // on_background_error - TODO
+            None, // on_stall_conditions_changed - TODO
+            None, // on_memtable_sealed - TODO
         )
     }
 }

Then you can remove the stub functions (lines 118-176).


1-198: Run cargo fmt to address formatting issues.

Rustfmt reports formatting differences at multiple locations. Please run cargo fmt to automatically fix these style issues.

Based on static analysis hints.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 153e294 and 35d8e62.

📒 Files selected for processing (1)
  • src/event_listener.rs (1 hunks)
🧰 Additional context used
🪛 GitHub Check: Rustfmt
src/event_listener.rs

[warning] 196-196:
Diff in /home/runner/work/rust-rocksdb/rust-rocksdb/src/event_listener.rs


[warning] 174-174:
Diff in /home/runner/work/rust-rocksdb/rust-rocksdb/src/event_listener.rs


[warning] 68-68:
Diff in /home/runner/work/rust-rocksdb/rust-rocksdb/src/event_listener.rs


[warning] 62-62:
Diff in /home/runner/work/rust-rocksdb/rust-rocksdb/src/event_listener.rs


[warning] 56-56:
Diff in /home/runner/work/rust-rocksdb/rust-rocksdb/src/event_listener.rs


[warning] 47-47:
Diff in /home/runner/work/rust-rocksdb/rust-rocksdb/src/event_listener.rs


[warning] 12-12:
Diff in /home/runner/work/rust-rocksdb/rust-rocksdb/src/event_listener.rs

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Security audit
  • GitHub Check: Linux
  • GitHub Check: macOS
  • GitHub Check: Windows
  • GitHub Check: Doctests
  • GitHub Check: Rustdoc-check
  • GitHub Check: Clippy

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/event_listener.rs (2)

25-63: Critical: Use-after-free hazard with FlushJobInfo.

This issue was previously identified in past reviews. The FlushJobInfo struct holds a raw pointer that is only valid during the RocksDB event callback. Users can store this struct beyond the callback scope, leading to dangling pointer access when methods like cf_name() or file_path() are called later.

The past review suggested three mitigation strategies:

  1. Add lifetime parameters to tie the struct to the callback scope
  2. Document prominently that FlushJobInfo must not outlive the callback
  3. Eagerly copy all data in the callback wrapper instead of passing the raw pointer

Please implement one of these approaches to prevent memory safety issues.


95-137: Major: Callbacks lack panic handling to prevent undefined behavior.

This issue was identified in previous reviews. If a user's EventListener implementation panics, the panic will unwind through the C/FFI boundary, which is undefined behavior in Rust.

The WARNING comment at lines 95-97 acknowledges this issue but states "Not fixing this issue for now." As suggested in past reviews, wrap the callback bodies with std::panic::catch_unwind to prevent UB:

🔎 Example panic handling for on_flush_begin
unsafe extern "C" fn on_flush_begin<E: EventListener>(
    ctx: *mut c_void,
    _db: *mut ffi::rocksdb_t,
    flush_job_info: *const ffi::rocksdb_flushjobinfo_t,
) {
    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        let listener = &*(ctx as *const E);
        let info = FlushJobInfo {
            inner: flush_job_info,
        };
        listener.on_flush_begin(&info);
    }));
}

Apply similar wrapping to on_flush_completed and the destructor.

🧹 Nitpick comments (1)
src/event_listener.rs (1)

139-197: Stub callbacks registered but not implemented.

Eight event callbacks are registered with RocksDB but contain only TODO placeholders:

  • Compaction events (begin/completed, subcompaction begin/completed)
  • External file ingestion
  • Background errors
  • Stall condition changes
  • Memtable sealing

While not a bug (RocksDB will simply invoke no-op functions), consider:

  1. Uncommenting the corresponding trait methods in EventListener (lines 101-108)
  2. Implementing the callback bodies to invoke those trait methods
  3. Creating wrapper types like FlushJobInfo for other event info structs

This would provide a complete event listener API for users.

Do you want me to generate the implementation pattern for the remaining callbacks?

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35d8e62 and 8f2b610.

📒 Files selected for processing (4)
  • src/db_options.rs (2 hunks)
  • src/event_listener.rs (1 hunks)
  • src/lib.rs (2 hunks)
  • tests/test_event_listener.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib.rs
  • tests/test_event_listener.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/event_listener.rs (2)
src/ffi_util.rs (1)
  • from_raw_parts (219-221)
tests/test_event_listener.rs (2)
  • on_flush_begin (28-31)
  • on_flush_completed (33-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: macOS
  • GitHub Check: Doctests
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: Rustdoc-check
  • GitHub Check: Clippy
🔇 Additional comments (4)
src/db_options.rs (2)

33-33: LGTM! Import correctly adds EventListenerHandle.

The import is properly scoped and necessary for the new add_event_listener method.


1854-1893: LGTM! Ownership transfer to RocksDB is correctly implemented.

The method properly transfers ownership of the event listener to RocksDB by:

  1. Calling into_raw() which sets the owned flag to false
  2. Passing the raw pointer to the FFI layer
  3. Preventing Rust's Drop from destroying the listener

The documentation and example are comprehensive and helpful.

src/event_listener.rs (2)

65-94: LGTM! EventListenerHandle ownership management is well-designed.

The handle correctly manages ownership with the owned flag:

  • from_raw assumes ownership (owned=true)
  • into_raw transfers ownership (owned=false)
  • Drop only destroys if owned

The Send and Sync implementations are appropriate for this use case.


199-218: LGTM! Factory function correctly constructs the event listener.

The new_event_listener function properly:

  1. Boxes the user's listener implementation for a stable pointer
  2. Registers all callbacks (both implemented and stub) with RocksDB
  3. Wraps the FFI pointer in a safe EventListenerHandle

The implementation is sound and provides a safe interface for users.

@AlexStocks
Copy link

AlexStocks commented Dec 21, 2025

GROK 代码审查报告(feat(eventlistener) support eventlistener interface)

关键要点

  • 存在潜在 Bug:有几个安全性和稳定性问题,主要集中在 FFI 回调和生命周期管理上。
  • 逻辑缺陷:部分回调未实现但注册了 stub 函数,可能导致 RocksDB 行为异常;FlushJobInfo 生命周期风险高。
  • 语法规范:整体良好,但有 Rust FFI 常见陷阱。

该 PR 为 rust-rocksdb 添加了 RocksDB EventListener 支持(目前仅 flush 事件),通过 FFI 桥接 C API。功能基本可用,但存在安全隐患,不建议直接合并到生产分支,需修复后再考虑。

1. Bug(潜在运行时问题)

  • 严重 Bug:FFI 回调 panic 导致 undefined behavior文件src/event_listener.rs行号(约):80-120(所有 unsafe extern "C" fn 如 on_flush_begin)
    Rust
    unsafe extern "C" fn on_flush_begin<E: EventListener>(...) {
        let listener = &*(ctx as *const E);
        listener.on_flush_begin(&info);
    }
    问题:如果 EventListener 实现中 panic,panic 会跨 FFI 边界传播到 C 代码,这是 undefined behavior(Rust 官方文档明确禁止)。 影响:可能导致进程崩溃、内存损坏。 建议:在每个回调包裹 std::panic::catch_unwind:
    Rust
    unsafe extern "C" fn on_flush_begin<E: EventListener>(...) {
        let _ = std::panic::catch_unwind(|| {
            let listener = &*(ctx as *const E);
            let info = FlushJobInfo { inner: flush_job_info };
            listener.on_flush_begin(&info);
        });
    }
  • 严重 Bug:FlushJobInfo 生命周期风险(use-after-free 可能)文件src/event_listener.rs行号(约):20-40(FlushJobInfo 定义)和回调中
    Rust
    pub struct FlushJobInfo {
        pub(crate) inner: *const ffi::rocksdb_flushjobinfo_t,
    }
    问题:RocksDB C API 文档明确指出 flushjobinfo_t 只在回调期间有效。用户若在回调中存储 FlushJobInfo 或其引用,回调返回后指针失效 → use-after-free。 影响:内存安全漏洞。 建议:在 FlushJobInfo 上添加文档警告,或提供 copy 方法提取数据(如 cf_name、file_path 已实现,但用户可能直接存 struct)。
  • 小 Bug:未实现回调注册了 stub文件src/event_listener.rs行号(约):200-210(new_event_listener 中 Some(on_compaction_begin::<E>) 等)
    Rust
    ffi::rocksdb_eventlistener_create(
        ...,
        Some(on_compaction_begin::<E>),  // stub 函数为空实现
        ...
    )
    问题:RocksDB 会调用这些回调,但 Rust 侧为空操作,可能丢失事件或误导用户。 建议:未实现回调传 None,避免 RocksDB 调用空函数。

2. 逻辑错误(设计或功能逻辑问题)

  • 逻辑缺陷:EventListenerHandle 所有权转移复杂文件src/db_options.rs行号(约):add_event_listener 方法
    Rust
    pub fn add_event_listener(&mut self, listener: EventListenerHandle) {
        let ptr = listener.into_raw();  // owned = false
        unsafe { ffi::rocksdb_options_add_eventlistener(self.inner, ptr); }
    }
    问题:into_raw 将 owned 设 false,避免 Drop 时双 free,但用户若忘记调用 into_raw,直接 drop listener 会 double destroy。 建议:API 设计更安全,如 Options::set_event_listener(self, listener: impl EventListener + 'static),内部 Box 并管理生命周期(参考 rocksdb-rs 官方 crate)。
  • 逻辑缺陷:测试路径硬编码文件tests/test_event_listener.rs问题:使用 "/tmp/rocksdb_test" 硬编码路径,多测试并发可能冲突。 建议:使用 tempfile 或独特路径。

3. 语法不当(代码风格、规范问题)

  • 规范良好:unsafe 块明确,Send/Sync 实现合理,测试完整。
  • 小建议
    • destructor 中 Box::from_raw 未处理 poison(panic 时可能泄漏)。
    • 未运行 cargo fmt(CodeRabbit 已建议并后续 commit 修复)。

总体建议

  • 存在 Bug:是的,主要两个严重问题(panic UB + FlushJobInfo 生命周期),必须修复。
  • 评分:7/10(功能可用,但安全隐患高)。
  • 建议不建议合并,需先修复 panic 安全和回调注册逻辑。
类别 | 文件/行号 | 问题描述 | 修复建议 | 优先级 -- | -- | -- | -- | -- Bug | event_listener.rs:80-120 | FFI 回调 panic UB | 包裹 catch_unwind | 高 Bug | event_listener.rs:20-40 | FlushJobInfo use-after-free 风险 | 加强文档或 copy 数据 | 高 Bug | event_listener.rs:200-210 | 未实现回调注册 stub | 传 None | 中 逻辑错误 | db_options.rs add_event_listener | 所有权转移复杂易错 | 内部 Box 管理 | 中

@AlexStocks
Copy link

GPT 审查报告
比较实在的缺陷/潜在 bug 点(按风险从高到低):

  1. 严重:panic 跨 FFI 边界是 UB(未修复)

PR 里自己也写了 WARNING:如果用户实现的 EventListener 在回调里 panic,会发生 Rust unwind 穿过 C/FFI 边界,这是未定义行为(UB),可能直接把进程搞挂或者出现诡异内存问题。现在的实现确实没有 catch_unwind。
GitHub

建议修复方式:

在每个 extern "C" 回调里用 std::panic::catch_unwind 包起来;

panic 后至少要吞掉(记录日志/计数),避免传播到 C;

(可选)把 panic 信息写到某个全局 hook 或 callback error handler。

  1. flush_job_info 指针未做空指针防御

回调里直接把 flush_job_info 塞进 FlushJobInfo { inner: flush_job_info },后续 cf_name()/file_path() 会无条件调用 ffi::rocksdb_flushjobinfo_* 并对返回指针做 slice。
GitHub

如果 RocksDB 在某些异常路径传了 NULL(我不敢断言一定不会),这会立刻 UB。

建议:

回调入口对 flush_job_info.is_null() 做 guard;

或者把 FlushJobInfo 设计成 Option / 在方法里遇到 null 返回空字符串/Result。

  1. FlushJobInfo::cf_name/file_path 对返回指针没做 null 检查

这里逻辑是:C 返回 ptr + size,然后 from_raw_parts(ptr as *const u8, size)。
GitHub

如果 C 在某些情况下返回 ptr = NULL, size = 0,理论上 from_raw_parts(NULL, 0) 是允许的;但如果是 ptr = NULL, size > 0 就直接 UB。

建议:

若 size > 0 && ptr.is_null() 则返回空/报错;

或者统一把空指针当成空字符串处理。

  1. EventListenerHandle 的 Send/Sync 标注可能过于乐观

EventListenerHandle 被 unsafe impl Send + Sync 了。
GitHub

从 Rust 侧看你约束了 E: Send + Sync,但 C 侧 rocksdb_eventlistener_t 本身是否能跨线程销毁/访问,这里并没有证明。RocksDB 的 event listener 回调通常可能发生在后台线程,这点倒是符合,但handle 的 Drop(destroy)在哪个线程发生不可控。

建议(保守做法):

不给 EventListenerHandle 自动标 Send/Sync,或者至少在文档里明确线程模型;

若要保留,最好给出安全性说明:为什么 destroy 和 C 对象线程安全。

  1. Options::add_event_listener 的所有权语义:正确但容易误用,文档需更明确

add_event_listener 消费 EventListenerHandle,内部 into_raw() 把 owned=false,避免 Rust Drop 再 destroy,然后把 ptr 交给 rocksdb_options_add_eventlistener。这个逻辑本身是合理的(避免 double free)。
GitHub

但这里有两个“坑”需要在文档写清楚:

一旦 add 进去,listener 生命周期由 RocksDB/C++ 管,Rust 侧不再能手动释放;

如果用户创建了 handle 但没 add,就由 Rust Drop 释放(这是当前行为)。

否则用户容易误以为 handle 还要保存在某处来“保持活着”。

  1. new_event_listener 里大量 Some(on_xxx::),但很多回调是 TODO 空实现

你把 compaction / ingestion / background_error / stall / memtable 等回调都传进去了,但目前函数体是 TODO 空实现。
GitHub

这不一定是 bug,但会误导使用者:他们以为实现 trait 对应方法会被调用,但实际上 trait 里也注释掉了这些方法,只剩 flush 两个默认实现。
GitHub

建议:

要么别把这些 TODO callback 注册进去(传 None),避免未来 ABI/行为不一致;

要么把 trait 方法补齐(即使默认空实现),并写清楚哪些 RocksDB 版本/特性可用。

结论

这个 PR 能跑通 flush 监听的主路径,但最大的问题是 #1:panic 跨 FFI 的 UB,属于必须修的安全缺陷。 其次是空指针/返回指针防御(#2/rust-rocksdb#3),以及 Send/Sync 的安全性说明(rust-rocksdb#4)。

直接给你一版可直接贴进 PR 的补丁级写法,目标只有两件事:

  1. panic 不得跨 FFI(catch_unwind)

  2. 所有来自 C 的指针先做 null guard

我按 “最小侵入、不改现有 API” 的原则来写。


一、FFI 回调统一加 catch_unwind

1️⃣ 一个通用的安全包装器(建议放在 event_listener.rs 顶部)

use std::panic::{catch_unwind, AssertUnwindSafe};

#[inline]
fn ffi_guard<F>(f: F)
where
F:
FnOnce(),
{
if let Err(err) = catch_unwind(AssertUnwindSafe(f)) {
// 这里不能 panic,只能吞掉
// 尽量不要分配复杂对象,避免二次失败
eprintln!("rust-rocksdb: panic in EventListener callback: {:?}", err);
}
}

说明:

  • AssertUnwindSafe 是必须的(trait 对象 + FFI 场景)

  • 不能 panic!、不能 unwrap

  • eprintln! 比 log 框架更安全(避免 logger 内部再 panic)


2️⃣ 在 extern "C" 回调里使用(以 flush 开始/结束为例)

原来(危险)

unsafe extern "C" fn on_flush_begin<E: EventListener>( ctx: *mut c_void, flush_job_info: *const ffi::rocksdb_flushjobinfo_t, ) { let listener = &*(ctx as *const E); let info = FlushJobInfo { inner: flush_job_info }; listener.on_flush_begin(&info); }

修改后(安全)

unsafe extern "C" fn on_flush_begin<E: EventListener>( ctx: *mut c_void, flush_job_info: *const ffi::rocksdb_flushjobinfo_t, ) { ffi_guard(|| { if ctx.is_null() { return; } if flush_job_info.is_null() { // RocksDB 理论上不该传 null,但防御式处理 return; }
    </span><span><span class="hljs-keyword">let</span></span><span> </span><span><span class="hljs-variable">listener</span></span><span> = &amp;*(ctx </span><span><span class="hljs-keyword">as</span></span><span> *</span><span><span class="hljs-keyword">const</span></span><span> E);
    </span><span><span class="hljs-keyword">let</span></span><span> </span><span><span class="hljs-variable">info</span></span><span> = FlushJobInfo { inner: flush_job_info };
    listener.</span><span><span class="hljs-title function_ invoke__">on_flush_begin</span></span><span>(&amp;info);
});

}

on_flush_completed 同样处理:

unsafe extern "C" fn on_flush_completed<E: EventListener>( ctx: *mut c_void, flush_job_info: *const ffi::rocksdb_flushjobinfo_t, ) { ffi_guard(|| { if ctx.is_null() || flush_job_info.is_null() { return; }
    </span><span><span class="hljs-keyword">let</span></span><span> </span><span><span class="hljs-variable">listener</span></span><span> = &amp;*(ctx </span><span><span class="hljs-keyword">as</span></span><span> *</span><span><span class="hljs-keyword">const</span></span><span> E);
    </span><span><span class="hljs-keyword">let</span></span><span> </span><span><span class="hljs-variable">info</span></span><span> = FlushJobInfo { inner: flush_job_info };
    listener.</span><span><span class="hljs-title function_ invoke__">on_flush_completed</span></span><span>(&amp;info);
});

}

🔒 结果:

  • Rust panic 永远不会穿过 C

  • ctx / info 任意为 null 都直接短路


二、FlushJobInfo 内部加 null guard(防 UB)

3️⃣ 给 FlushJobInfo 一个内部 helper

impl FlushJobInfo { #[inline] fn is_null(&self) -> bool { self.inner.is_null() } }

4️⃣ 修改 cf_name()(同理适用于 file_path()

原来(可能 UB)

pub fn cf_name(&self) -> &str { unsafe { let mut len = 0; let ptr = ffi::rocksdb_flushjobinfo_cf_name(self.inner, &mut len); let slice = std::slice::from_raw_parts(ptr as *const u8, len); std::str::from_utf8_unchecked(slice) } }

修改后(完全防御)

pub fn cf_name(&self) -> &str { if self.inner.is_null() { return ""; }
</span><span><span class="hljs-keyword">unsafe</span></span><span> {
    </span><span><span class="hljs-keyword">let</span></span><span> </span><span><span class="hljs-keyword">mut </span></span><span><span class="hljs-variable">len</span></span><span> = </span><span><span class="hljs-number">0</span></span><span>;
    </span><span><span class="hljs-keyword">let</span></span><span> </span><span><span class="hljs-variable">ptr</span></span><span> = ffi::</span><span><span class="hljs-title function_ invoke__">rocksdb_flushjobinfo_cf_name</span></span><span>(</span><span><span class="hljs-keyword">self</span></span><span>.inner, &amp;</span><span><span class="hljs-keyword">mut</span></span><span> len);

    </span><span><span class="hljs-keyword">if</span></span><span> ptr.</span><span><span class="hljs-title function_ invoke__">is_null</span></span><span>() || len == </span><span><span class="hljs-number">0</span></span><span> {
        </span><span><span class="hljs-keyword">return</span></span><span> </span><span><span class="hljs-string">""</span></span><span>;
    }

    </span><span><span class="hljs-keyword">let</span></span><span> </span><span><span class="hljs-variable">slice</span></span><span> = std::slice::</span><span><span class="hljs-title function_ invoke__">from_raw_parts</span></span><span>(ptr </span><span><span class="hljs-keyword">as</span></span><span> *</span><span><span class="hljs-keyword">const</span></span><span> </span><span><span class="hljs-type">u8</span></span><span>, len);
    </span><span><span class="hljs-keyword">match</span></span><span> std::</span><span><span class="hljs-type">str</span></span><span>::</span><span><span class="hljs-title function_ invoke__">from_utf8</span></span><span>(slice) {
        </span><span><span class="hljs-title function_ invoke__">Ok</span></span><span>(s) =&gt; s,
        </span><span><span class="hljs-title function_ invoke__">Err</span></span><span>(_) =&gt; </span><span><span class="hljs-string">""</span></span><span>,
    }
}

}

file_path() 同样:

pub fn file_path(&self) -> &str { if self.inner.is_null() { return ""; }
</span><span><span class="hljs-keyword">unsafe</span></span><span> {
    </span><span><span class="hljs-keyword">let</span></span><span> </span><span><span class="hljs-keyword">mut </span></span><span><span class="hljs-variable">len</span></span><span> = </span><span><span class="hljs-number">0</span></span><span>;
    </span><span><span class="hljs-keyword">let</span></span><span> </span><span><span class="hljs-variable">ptr</span></span><span> = ffi::</span><span><span class="hljs-title function_ invoke__">rocksdb_flushjobinfo_file_path</span></span><span>(</span><span><span class="hljs-keyword">self</span></span><span>.inner, &amp;</span><span><span class="hljs-keyword">mut</span></span><span> len);

    </span><span><span class="hljs-keyword">if</span></span><span> ptr.</span><span><span class="hljs-title function_ invoke__">is_null</span></span><span>() || len == </span><span><span class="hljs-number">0</span></span><span> {
        </span><span><span class="hljs-keyword">return</span></span><span> </span><span><span class="hljs-string">""</span></span><span>;
    }

    </span><span><span class="hljs-keyword">let</span></span><span> </span><span><span class="hljs-variable">slice</span></span><span> = std::slice::</span><span><span class="hljs-title function_ invoke__">from_raw_parts</span></span><span>(ptr </span><span><span class="hljs-keyword">as</span></span><span> *</span><span><span class="hljs-keyword">const</span></span><span> </span><span><span class="hljs-type">u8</span></span><span>, len);
    </span><span><span class="hljs-keyword">match</span></span><span> std::</span><span><span class="hljs-type">str</span></span><span>::</span><span><span class="hljs-title function_ invoke__">from_utf8</span></span><span>(slice) {
        </span><span><span class="hljs-title function_ invoke__">Ok</span></span><span>(s) =&gt; s,
        </span><span><span class="hljs-title function_ invoke__">Err</span></span><span>(_) =&gt; </span><span><span class="hljs-string">""</span></span><span>,
    }
}

}

✔️ 保证:

  • (NULL, >0) 不会 UB

  • 非 UTF-8 不 panic

  • 行为稳定、对用户透明


三、(可选但推荐)在 trait 文档里加一行安全声明

/// ⚠️ Safety /// Implementations MUST NOT panic. Panics will be caught and ignored /// to prevent unwinding across FFI boundaries. pub trait EventListener: Send + Sync + 'static { ... }

四、最终效果总结

风险 | 修复状态 -- | -- panic 穿过 FFI | ✅ 完全修复 C 指针 null UB | ✅ 防御 UTF-8 非法数据 | ✅ 不 panic API 兼容性 | ✅ 无破坏 行为一致性 | ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants