Ruby扩展开发:如何在C/Rust侧保存引用避免GC回收?
问题:Ruby扩展缓存对象的GC回收问题
我正在开发一个客户端缓存库,使用HashMap缓存对象,以下是相关代码示例:
Rust 实现代码
fn insert(key: RString, obj: AnyObject) -> AnyObject { let rust_struct = self.get_data_mut(&*STORE_WRAPPER); rust_struct .hash_map .insert(key.unwrap().to_string(), Arc::new(obj)); NilClass::new().into() } pub extern "C" fn Init_ruby_example() { Class::new("ObjectStore", None).define(|klass| { klass.def("insert", insert); }); }
Ruby 使用示例
store = ObjectStore.new def insert(store) store.insert('key1', 'some-val') store.insert('key2', Bar.new) end
查询逻辑
insert(store) store.get('key1') # 期望返回'some-val' store.get('key2') # 期望返回Bar对象
当前遇到的问题是:缓存的'some-val'和Bar对象可能因为Ruby侧没有引用而被GC回收。我可以固定值对象避免GC压缩时移动,但不知道如何在Ruby侧保持引用防止回收。rb_gc_register_address和rb_gc_unregister_address看似可行,但后者需要遍历所有注册对象,性能较差,请问该如何解决?
解决方案
1. 利用Ruby GC的标记机制(推荐方案)
Ruby的GC采用标记-清除算法,只要你的ObjectStore实例在Ruby侧有引用,你可以通过扩展的数据类型标记回调,让GC在标记ObjectStore时自动标记缓存中的所有Ruby对象,这样这些对象就不会被回收。
具体实现步骤:
- 给
ObjectStore关联的Rust结构体定义一个dmark回调函数,在函数内部遍历HashMap中的所有AnyObject,调用rb_gc_mark标记它们。 - 通过
rb_data_typed_struct将这个标记逻辑绑定到ObjectStore类上,确保GC能触发标记。
示例代码:
use rb_sys::{self, rb_data_type_t, rb_data_type_funcs_t, rb_gc_mark}; use std::collections::HashMap; use std::sync::Arc; #[derive(Default)] struct ObjectStoreData { hash_map: HashMap<String, Arc<AnyObject>>, } // 定义数据类型的函数表 static mut OBJECT_STORE_DATA_TYPE: rb_data_type_t = rb_data_type_t { wrap_struct_name: b"ObjectStoreData\0".as_ptr() as _, function: Some(rb_data_type_funcs_t { dmark: Some(object_store_dmark), dfree: Some(object_store_dfree), dsize: None, compact: None, reserved: [0; 4], }), flags: rb_sys::RUBY_TYPED_FREE_IMMEDIATELY, reserved: [0; 1], }; // GC标记回调:遍历缓存对象并标记 unsafe extern "C" fn object_store_dmark(data: *mut std::os::raw::c_void) { let store_data = &mut *(data as *mut ObjectStoreData); for obj in store_data.hash_map.values() { rb_gc_mark(obj.as_rb_value()); } } // 内存释放回调 unsafe extern "C" fn object_store_dfree(data: *mut std::os::raw::c_void) { let _store_data = Box::from_raw(data as *mut ObjectStoreData); } // 初始化ObjectStore类时绑定数据结构 pub extern "C" fn Init_ruby_example() { let klass = Class::new("ObjectStore", None); klass.define(|klass| { klass.def("initialize", initialize); klass.def("insert", insert); klass.def("get", get); }); } // 初始化方法:绑定数据结构到Ruby实例 fn initialize(klass: Class) -> AnyObject { let data = ObjectStoreData::default(); DataWrapStruct::new(klass, data, &mut OBJECT_STORE_DATA_TYPE).into() } // 修改后的insert方法 fn insert(key: RString, obj: AnyObject, self_obj: AnyObject) -> AnyObject { let store_data = self_obj.get_data_mut::<ObjectStoreData>(&mut OBJECT_STORE_DATA_TYPE); store_data.hash_map.insert(key.unwrap().to_string(), Arc::new(obj)); NilClass::new().into() }
2. 用Ruby实例变量持有引用
在ObjectStore的Ruby实例中维护一个隐藏的实例变量(如@__cached_objects),将所有插入的对象存入这个数组。Ruby的GC会自动跟踪数组内的对象,避免它们被回收。
实现时,在Rust的insert方法中,除了写入HashMap,还要把对象添加到这个实例变量的数组里:
fn insert(key: RString, obj: AnyObject, self_obj: AnyObject) -> AnyObject { let store_data = self_obj.get_data_mut::<ObjectStoreData>(&mut OBJECT_STORE_DATA_TYPE); store_data.hash_map.insert(key.unwrap().to_string(), Arc::new(obj.clone())); // 获取或创建缓存数组 let cached_arr = match self_obj.instance_variable_get("@__cached_objects") { Ok(arr) => arr, Err(_) => { let arr = Array::new(); self_obj.instance_variable_set("@__cached_objects", arr.clone()).unwrap(); arr } }; cached_arr.push(obj).unwrap(); NilClass::new().into() }
这种方式无需手动处理GC标记,但要注意:如果缓存对象数量大,数组会占用额外内存;删除缓存时需要从数组中移除对应对象,否则会造成内存泄漏。
3. 不推荐使用rb_gc_register_address
rb_gc_register_address需要手动管理对象的注册与注销,注销时遍历所有对象会带来明显的性能开销,且容易因遗漏注销操作导致内存泄漏,因此不建议采用这种方案。
内容的提问来源于stack exchange,提问作者Yang
相关产品推荐
相关产品推荐

