Skip to content

Commit 9312864

Browse files
committed
Add optimized_struct option for fast Struct serialization
Adds a C-level fast path for serializing and deserializing Ruby Struct types, bypassing Ruby proc callbacks entirely. Usage: factory.register_type(0x01, MyStruct, optimized_struct: true) Benefits: - Directly accesses struct fields via RSTRUCT_GET in C - No Ruby method calls for field access - No proc allocation or invocation overhead - Supports both regular and keyword_init structs The wire format is identical to the recursive proc-based approach, ensuring compatibility with existing serialized data. This provides significant performance improvements for applications that serialize many Struct objects.
1 parent 199e88f commit 9312864

File tree

6 files changed

+437
-0
lines changed

6 files changed

+437
-0
lines changed

ext/msgpack/factory_class.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,27 @@ static VALUE Factory_register_type_internal(VALUE self, VALUE rb_ext_type, VALUE
250250
if(RTEST(rb_hash_aref(options, ID2SYM(rb_intern("recursive"))))) {
251251
flags |= MSGPACK_EXT_RECURSIVE;
252252
}
253+
254+
/* optimized_struct: true enables C-level fast path for Struct types */
255+
if (RTEST(rb_hash_aref(options, ID2SYM(rb_intern("optimized_struct"))))) {
256+
/* Verify it's actually a Struct subclass */
257+
if (!(rb_obj_is_kind_of(ext_module, rb_cClass) &&
258+
RTEST(rb_class_inherited_p(ext_module, rb_cStruct)))) {
259+
rb_raise(rb_eArgError, "optimized_struct: true requires a Struct subclass");
260+
}
261+
/* Verify packer/unpacker procs weren't also provided */
262+
if (RTEST(packer_proc) || RTEST(unpacker_proc)) {
263+
rb_raise(rb_eArgError, "optimized_struct: true cannot be used with packer or unpacker options");
264+
}
265+
/* Verify recursive wasn't also provided - optimized_struct handles recursion automatically */
266+
if (flags & MSGPACK_EXT_RECURSIVE) {
267+
rb_raise(rb_eArgError, "optimized_struct: true cannot be used with recursive option");
268+
}
269+
flags |= MSGPACK_EXT_STRUCT_FAST_PATH;
270+
/* Store the class itself - C code uses it directly */
271+
packer_proc = ext_module;
272+
unpacker_proc = ext_module;
273+
}
253274
}
254275

255276
msgpack_packer_ext_registry_put(self, &fc->pkrg, ext_module, ext_type, flags, packer_proc);

ext/msgpack/packer.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,28 @@ void msgpack_packer_write_hash_value(msgpack_packer_t* pk, VALUE v)
9191
rb_hash_foreach(v, write_hash_foreach, (VALUE) pk);
9292
}
9393

94+
/* Pack a Struct's fields directly using C API, bypassing Ruby callbacks */
95+
struct msgpack_packer_struct_args_t;
96+
typedef struct msgpack_packer_struct_args_t msgpack_packer_struct_args_t;
97+
struct msgpack_packer_struct_args_t {
98+
msgpack_packer_t* pk;
99+
VALUE v;
100+
};
101+
102+
static VALUE msgpack_packer_write_struct_fields_protected(VALUE value)
103+
{
104+
msgpack_packer_struct_args_t *args = (msgpack_packer_struct_args_t *)value;
105+
msgpack_packer_t* pk = args->pk;
106+
VALUE v = args->v;
107+
108+
long len = RSTRUCT_LEN(v);
109+
for (int i = 0; i < len; i++) {
110+
VALUE field = RSTRUCT_GET(v, i);
111+
msgpack_packer_write_value(pk, field);
112+
}
113+
return Qnil;
114+
}
115+
94116
struct msgpack_call_proc_args_t;
95117
typedef struct msgpack_call_proc_args_t msgpack_call_proc_args_t;
96118
struct msgpack_call_proc_args_t {
@@ -114,6 +136,33 @@ bool msgpack_packer_try_write_with_ext_type_lookup(msgpack_packer_t* pk, VALUE v
114136
return false;
115137
}
116138

139+
if(ext_flags & MSGPACK_EXT_STRUCT_FAST_PATH) {
140+
/* Fast path for Struct: directly access fields in C, no Ruby callbacks */
141+
VALUE held_buffer = MessagePack_Buffer_hold(&pk->buffer);
142+
143+
msgpack_buffer_t parent_buffer = pk->buffer;
144+
msgpack_buffer_init(PACKER_BUFFER_(pk));
145+
146+
/* Write struct fields with exception handling */
147+
int exception_occured = 0;
148+
msgpack_packer_struct_args_t args = { pk, v };
149+
rb_protect(msgpack_packer_write_struct_fields_protected, (VALUE)&args, &exception_occured);
150+
151+
if (exception_occured) {
152+
msgpack_buffer_destroy(PACKER_BUFFER_(pk));
153+
pk->buffer = parent_buffer;
154+
rb_jump_tag(exception_occured); // re-raise the exception
155+
} else {
156+
VALUE payload = msgpack_buffer_all_as_string(PACKER_BUFFER_(pk));
157+
msgpack_buffer_destroy(PACKER_BUFFER_(pk));
158+
pk->buffer = parent_buffer;
159+
msgpack_packer_write_ext(pk, ext_type, payload);
160+
}
161+
162+
RB_GC_GUARD(held_buffer);
163+
return true;
164+
}
165+
117166
if(ext_flags & MSGPACK_EXT_RECURSIVE) {
118167
VALUE held_buffer = MessagePack_Buffer_hold(&pk->buffer);
119168

ext/msgpack/packer_ext_registry.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "ruby.h"
2323

2424
#define MSGPACK_EXT_RECURSIVE 0b0001
25+
#define MSGPACK_EXT_STRUCT_FAST_PATH 0b0010
2526

2627
struct msgpack_packer_ext_registry_t;
2728
typedef struct msgpack_packer_ext_registry_t msgpack_packer_ext_registry_t;

ext/msgpack/unpacker.c

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,13 @@ static inline int object_complete_ext(msgpack_unpacker_t* uk, int ext_type, VALU
231231
VALUE proc = msgpack_unpacker_ext_registry_lookup(uk->ext_registry, ext_type, &ext_flags);
232232

233233
if(proc != Qnil) {
234+
/* Handle struct fast path for empty structs (0 fields) */
235+
if (ext_flags & MSGPACK_EXT_STRUCT_FAST_PATH) {
236+
VALUE struct_class = proc;
237+
VALUE obj = rb_class_new_instance(0, NULL, struct_class);
238+
return object_complete(uk, obj);
239+
}
240+
234241
VALUE obj;
235242
VALUE arg = (str == Qnil ? rb_str_buf_new(0) : str);
236243
int raised;
@@ -371,6 +378,70 @@ static inline int read_raw_body_begin(msgpack_unpacker_t* uk, int raw_type)
371378

372379
if(!(raw_type == RAW_TYPE_STRING || raw_type == RAW_TYPE_BINARY)) {
373380
proc = msgpack_unpacker_ext_registry_lookup(uk->ext_registry, raw_type, &ext_flags);
381+
382+
if(proc != Qnil && ext_flags & MSGPACK_EXT_STRUCT_FAST_PATH) {
383+
/* Fast path for Struct: proc is actually the Struct class
384+
* Read fields directly and construct struct in C */
385+
VALUE struct_class = proc;
386+
uk->last_object = Qnil;
387+
reset_head_byte(uk);
388+
uk->reading_raw_remaining = 0;
389+
390+
/* Get struct members */
391+
VALUE members = rb_struct_s_members(struct_class);
392+
long num_fields = RARRAY_LEN(members);
393+
394+
/* Check if this is a keyword_init struct (Ruby 2.7+) */
395+
VALUE keyword_init = Qfalse;
396+
if (rb_respond_to(struct_class, rb_intern("keyword_init?"))) {
397+
keyword_init = rb_funcall(struct_class, rb_intern("keyword_init?"), 0);
398+
}
399+
400+
/* Push a recursive marker so nested reads don't prematurely return */
401+
_msgpack_unpacker_stack_push(uk, STACK_TYPE_RECURSIVE, 1, Qnil);
402+
403+
VALUE obj;
404+
if (num_fields == 0) {
405+
/* Special case for empty structs */
406+
obj = rb_class_new_instance(0, NULL, struct_class);
407+
} else if (RTEST(keyword_init)) {
408+
/* For keyword_init structs, build a hash with member names as keys */
409+
VALUE kwargs = rb_hash_new();
410+
for (long i = 0; i < num_fields; i++) {
411+
int ret = msgpack_unpacker_read(uk, 0);
412+
if (ret < 0) {
413+
msgpack_unpacker_stack_pop(uk);
414+
return ret;
415+
}
416+
VALUE key = rb_ary_entry(members, i);
417+
rb_hash_aset(kwargs, key, uk->last_object);
418+
}
419+
/* Call new with keyword arguments */
420+
obj = rb_class_new_instance_kw(1, &kwargs, struct_class, RB_PASS_KEYWORDS);
421+
} else {
422+
/* For regular structs, use positional arguments
423+
* Use RB_ALLOCV to avoid stack overflow with large structs */
424+
VALUE allocv_holder;
425+
VALUE *values = RB_ALLOCV_N(VALUE, allocv_holder, num_fields);
426+
for (int i = 0; i < num_fields; i++) {
427+
int ret = msgpack_unpacker_read(uk, 0);
428+
if (ret < 0) {
429+
msgpack_unpacker_stack_pop(uk);
430+
RB_ALLOCV_END(allocv_holder);
431+
return ret;
432+
}
433+
values[i] = uk->last_object;
434+
}
435+
obj = rb_class_new_instance((int)num_fields, values, struct_class);
436+
RB_ALLOCV_END(allocv_holder);
437+
}
438+
439+
RB_GC_GUARD(struct_class);
440+
RB_GC_GUARD(members);
441+
msgpack_unpacker_stack_pop(uk);
442+
return object_complete(uk, obj);
443+
}
444+
374445
if(proc != Qnil && ext_flags & MSGPACK_EXT_RECURSIVE) {
375446
VALUE obj;
376447
uk->last_object = Qnil;

ext/msgpack/unpacker_ext_registry.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "ruby.h"
2323

2424
#define MSGPACK_EXT_RECURSIVE 0b0001
25+
#define MSGPACK_EXT_STRUCT_FAST_PATH 0b0010
2526

2627
struct msgpack_unpacker_ext_registry_t;
2728
typedef struct msgpack_unpacker_ext_registry_t msgpack_unpacker_ext_registry_t;

0 commit comments

Comments
 (0)