1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
use crate::rubysys::{
st::StTable,
types::{c_int, c_long, c_void, size_t, ssize_t, CallbackPtr, Value},
};
#[cfg_attr(rutie_dllimport, link(name = "rutie_ruby"))]
extern "C" {
// void
// rb_gc_adjust_memory_usage(ssize_t diff)
pub fn rb_gc_adjust_memory_usage(diff: ssize_t);
// size_t
// rb_gc_count(void)
pub fn rb_gc_count() -> size_t;
// VALUE
// rb_gc_disable(void)
pub fn rb_gc_disable() -> Value;
// VALUE
// rb_gc_enable(void)
pub fn rb_gc_enable() -> Value;
// void
// rb_gc_mark(VALUE ptr)
pub fn rb_gc_mark(value: Value);
// void
// rb_gc_mark_movable(VALUE ptr)
pub fn rb_gc_mark_movable(value: Value);
// VALUE
// rb_gc_location(VALUE value)
pub fn rb_gc_location(value: Value) -> Value;
// void
// rb_gc_mark_maybe(VALUE obj)
pub fn rb_gc_mark_maybe(obj: Value);
// void
// rb_gc_register_address(VALUE *addr)
pub fn rb_gc_register_address(addr: CallbackPtr);
// void
// rb_gc_register_mark_object(VALUE obj)
pub fn rb_gc_register_mark_object(obj: Value);
// VALUE
// rb_gc_start(void)
pub fn rb_gc_start() -> Value;
// size_t
// rb_gc_stat(VALUE key)
pub fn rb_gc_stat(key: Value) -> size_t;
// void
// rb_gc_unregister_address(VALUE *addr)
pub fn rb_gc_unregister_address(addr: CallbackPtr);
// VALUE
// rb_define_finalizer(VALUE obj, VALUE block)
pub fn rb_define_finalizer(object: Value, block: Value) -> Value;
// VALUE
// rb_gc_latest_gc_info(VALUE key)
//
// `key` is a Hash to fill or a Symbol to look up.
pub fn rb_gc_latest_gc_info(key: Value) -> Value;
// void
// rb_gc_writebarrier(VALUE a, VALUE b)
pub fn rb_gc_writebarrier(parent: Value, child: Value);
// void
// rb_gc_writebarrier_unprotect(VALUE obj)
pub fn rb_gc_writebarrier_unprotect(object: Value);
// VALUE
// rb_memory_id(VALUE obj)
pub fn rb_memory_id(object: Value) -> Value;
// VALUE
// rb_undefine_finalizer(VALUE obj)
pub fn rb_undefine_finalizer(object: Value) -> Value;
}
// `ruby/internal/gc.h`: the rest of the GC API, mostly for C-level code
// (marking objects held in `st_table`s or C arrays).
#[cfg_attr(rutie_dllimport, link(name = "rutie_ruby"))]
extern "C" {
// int
// rb_during_gc(void)
pub fn rb_during_gc() -> c_int;
// void
// rb_gc(void)
//
// A full, immediate GC (what `GC.start` does with its default options).
pub fn rb_gc();
// void
// rb_gc_copy_finalizer(VALUE dst, VALUE src)
//
// Gives `dst` the finalizers of `src`.
pub fn rb_gc_copy_finalizer(dst: Value, src: Value);
// void
// rb_gc_mark_locations(const VALUE *start, const VALUE *end)
//
// `rb_gc_mark_maybe` for each `VALUE` from `start` up to `end`
// (excluded); only during marking.
pub fn rb_gc_mark_locations(start: *const Value, end: *const Value);
// void
// rb_gc_update_tbl_refs(st_table *ptr)
//
// Updates the values of a table marked with `rb_mark_tbl_no_pin` after
// compaction.
pub fn rb_gc_update_tbl_refs(table: *mut StTable);
// void
// rb_global_variable(VALUE *)
//
// The same as `rb_gc_register_address`.
pub fn rb_global_variable(address: *mut Value);
// void
// rb_mark_hash(struct st_table *tbl)
//
// Marks the keys and values of a table of `VALUE`s; only during marking.
pub fn rb_mark_hash(table: *mut StTable);
// void
// rb_mark_set(struct st_table *tbl)
//
// Marks the keys of a table.
pub fn rb_mark_set(table: *mut StTable);
// void
// rb_mark_tbl(struct st_table *tbl)
//
// Marks (and pins) the values of a table.
pub fn rb_mark_tbl(table: *mut StTable);
// void
// rb_mark_tbl_no_pin(struct st_table *tbl)
//
// Marks the values of a table, letting compaction move them (see
// `rb_gc_update_tbl_refs`).
pub fn rb_mark_tbl_no_pin(table: *mut StTable);
// void
// rb_memerror(void)
//
// Raises a `NoMemoryError`.
pub fn rb_memerror() -> !;
}
// `ruby/internal/xmalloc.h` and `ruby/internal/memory.h`: memory allocated
// by Ruby, which counts towards the GC's malloc limit (and may start a GC).
// The allocators raise a `NoMemoryError` instead of returning NULL; memory
// from them must be freed with `ruby_xfree`.
#[cfg_attr(rutie_dllimport, link(name = "rutie_ruby"))]
extern "C" {
// void *
// ruby_xcalloc(size_t nelems, size_t elemsiz)
pub fn ruby_xcalloc(nelems: size_t, elemsiz: size_t) -> *mut c_void;
// void
// ruby_xfree(void *ptr)
pub fn ruby_xfree(ptr: *mut c_void);
// void *
// ruby_xmalloc(size_t size)
pub fn ruby_xmalloc(size: size_t) -> *mut c_void;
// void *
// ruby_xmalloc2(size_t nelems, size_t elemsiz)
//
// `nelems * elemsiz` bytes, raising an `ArgumentError` on overflow.
pub fn ruby_xmalloc2(nelems: size_t, elemsiz: size_t) -> *mut c_void;
// void *
// ruby_xrealloc(void *ptr, size_t newsiz)
pub fn ruby_xrealloc(ptr: *mut c_void, newsiz: size_t) -> *mut c_void;
// void *
// ruby_xrealloc2(void *ptr, size_t newelems, size_t newsiz)
pub fn ruby_xrealloc2(ptr: *mut c_void, newelems: size_t, newsiz: size_t) -> *mut c_void;
// void *
// rb_alloc_tmp_buffer(volatile VALUE *store, long len)
//
// A temporary buffer of `len` bytes (`ALLOCV`) owned by a hidden object
// stored in `*store`, which must stay on the stack (or be marked) while
// the buffer is used. The GC marks any `VALUE`s in the buffer. Free it
// early with `rb_free_tmp_buffer`.
pub fn rb_alloc_tmp_buffer(store: *mut Value, len: c_long) -> *mut c_void;
// void *
// rb_alloc_tmp_buffer_with_count(volatile VALUE *store, size_t len, size_t count)
//
// Like `rb_alloc_tmp_buffer`; `count` is how many `VALUE`-sized words of
// the buffer the GC scans (`rb_alloc_tmp_buffer` passes all of them).
pub fn rb_alloc_tmp_buffer_with_count(
store: *mut Value,
len: size_t,
count: size_t,
) -> *mut c_void;
// void
// rb_free_tmp_buffer(volatile VALUE *store)
//
// Frees the buffer of `*store` and sets it to `Qfalse`.
pub fn rb_free_tmp_buffer(store: *mut Value);
// void
// ruby_malloc_size_overflow(size_t x, size_t y)
//
// Raises the `ArgumentError` for `x * y` overflowing.
pub fn ruby_malloc_size_overflow(x: size_t, y: size_t) -> !;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
rubysys::{
st,
typed_data::{self, RbDataType, RbDataTypeFunction},
},
types::c_char,
};
use std::ptr;
// A C-level object holding `VALUE`s in an `st_table` and an array.
struct Holder {
table: *mut StTable,
values: [Value; 2],
}
extern "C" fn mark_holder(data: *mut c_void) {
let holder = unsafe { &mut *(data as *mut Holder) };
unsafe {
rb_mark_hash(holder.table);
rb_mark_set(holder.table);
rb_mark_tbl(holder.table);
rb_mark_tbl_no_pin(holder.table);
rb_gc_mark_locations(holder.values.as_ptr(), holder.values.as_ptr().add(2));
assert_eq!(rb_during_gc(), 1);
}
}
static HOLDER_TYPE: RbDataType = RbDataType {
wrap_struct_name: b"RutieGcTestHolder\0".as_ptr() as *const c_char,
function: RbDataTypeFunction {
dmark: Some(mark_holder),
dfree: None,
dsize: None,
reserved: [ptr::null_mut(); 2],
},
parent: ptr::null(),
data: ptr::null_mut(),
flags: Value { value: 0 },
};
#[test]
fn test_marking_and_allocation() {
crate::on_ruby_thread(|| unsafe {
assert_eq!(rb_during_gc(), 0);
let key = crate::binding::vm::eval_string("'key' * 2");
let value = crate::binding::vm::eval_string("'value' * 2");
let table = st::rb_st_init_numtable_with_size(1);
st::rb_st_add_direct(table, key.value as _, value.value as _);
// Leaked: the holder lives as long as the test process.
let holder = Box::leak(Box::new(Holder {
table,
values: [key, value],
}));
let object = typed_data::rb_data_typed_object_wrap(
Value::from(0),
holder as *mut Holder as *mut c_void,
&HOLDER_TYPE,
);
rb_gc_register_mark_object(object);
rb_gc();
rb_gc_update_tbl_refs(table);
assert_eq!(crate::binding::string::value_to_string(value), "valuevalue");
let global = Box::leak(Box::new(crate::binding::vm::eval_string("[1]")));
rb_global_variable(global);
rb_gc();
let memory = ruby_xmalloc(16) as *mut u8;
*memory.add(15) = 1;
let memory = ruby_xrealloc(memory as *mut c_void, 32) as *mut u8;
assert_eq!(*memory.add(15), 1);
let memory = ruby_xrealloc2(memory as *mut c_void, 8, 8) as *mut u8;
assert_eq!(*memory.add(15), 1);
ruby_xfree(memory as *mut c_void);
let zeroed = ruby_xcalloc(4, 8) as *mut u64;
assert_eq!(*zeroed.add(3), 0);
ruby_xfree(zeroed as *mut c_void);
ruby_xfree(ruby_xmalloc2(3, 5));
// Size overflows raise an `ArgumentError`.
let overflow = crate::binding::vm::protect_value(|| {
ruby_malloc_size_overflow(usize::MAX, 2);
});
assert!(overflow.is_err());
let error = crate::binding::vm::protect_value(|| {
ruby_xmalloc2(usize::MAX, 2);
Value::from(0)
});
assert!(error.is_err());
let mut store = Value::from(0);
let buffer = rb_alloc_tmp_buffer(&mut store, 64) as *mut u8;
*buffer.add(63) = 7;
assert!(store.value != 0);
rb_free_tmp_buffer(&mut store);
assert!(store.is_false());
let buffer = rb_alloc_tmp_buffer_with_count(&mut store, 16, 2) as *mut Value;
*buffer = key;
rb_gc();
rb_free_tmp_buffer(&mut store);
});
}
}