2626void msgpack_packer_init (msgpack_packer_t * pk )
2727{
2828 msgpack_buffer_init (PACKER_BUFFER_ (pk ));
29+ pk -> ref_table = NULL ;
30+ pk -> next_ref_id = 1 ; /* 1-indexed */
2931}
3032
3133void msgpack_packer_destroy (msgpack_packer_t * pk )
3234{
3335 msgpack_buffer_destroy (PACKER_BUFFER_ (pk ));
36+ if (pk -> ref_table ) {
37+ st_free_table (pk -> ref_table );
38+ pk -> ref_table = NULL ;
39+ }
3440}
3541
3642void msgpack_packer_mark (msgpack_packer_t * pk )
@@ -46,6 +52,68 @@ void msgpack_packer_reset(msgpack_packer_t* pk)
4652 msgpack_buffer_clear (PACKER_BUFFER_ (pk ));
4753
4854 pk -> buffer_ref = Qnil ;
55+
56+ /* Reset ref tracking state */
57+ if (pk -> ref_table ) {
58+ st_clear (pk -> ref_table );
59+ }
60+ pk -> next_ref_id = 1 ;
61+ }
62+
63+ /*
64+ * Write a back-reference to a previously serialized object.
65+ * Wire format: ext type 127 followed by msgpack integer ref_id
66+ * We use fixext 1 with a 0 byte as a marker, then write the ref_id as a normal msgpack int.
67+ */
68+ static void msgpack_packer_write_back_ref (msgpack_packer_t * pk , long ref_id )
69+ {
70+ /* fixext 1, type 127, payload 0x01 (marker for back-ref) */
71+ msgpack_buffer_ensure_writable (PACKER_BUFFER_ (pk ), 3 );
72+ msgpack_buffer_write_2 (PACKER_BUFFER_ (pk ), 0xd4 , MSGPACK_EXT_REF_TYPE );
73+ msgpack_buffer_write_1 (PACKER_BUFFER_ (pk ), 0x01 );
74+ /* Write ref_id as a variable-length msgpack integer */
75+ msgpack_packer_write_long (pk , ref_id );
76+ }
77+
78+ /*
79+ * Write a new reference marker followed by the object.
80+ * Wire format: ext type 127 with payload = [nil, serialized_object]
81+ * The nil indicates this is a new ref (vs back-ref which has positive int).
82+ */
83+ static void msgpack_packer_write_new_ref_header (msgpack_packer_t * pk )
84+ {
85+ /* We write: ext header (variable len) + nil (1 byte) + object (variable)
86+ * Since we don't know the total length yet, we use a different approach:
87+ * Write nil as ext payload marker, then the object follows in the stream.
88+ *
89+ * Actually, let's use fixext 1 with nil (0xc0) as the 1-byte payload.
90+ * The unpacker will see ext type 127 with payload [0xc0] and know it's a new ref,
91+ * then read the next object from the stream.
92+ */
93+ msgpack_buffer_ensure_writable (PACKER_BUFFER_ (pk ), 3 );
94+ msgpack_buffer_write_2 (PACKER_BUFFER_ (pk ), 0xd4 , MSGPACK_EXT_REF_TYPE ); /* fixext 1, type 127 */
95+ msgpack_buffer_write_1 (PACKER_BUFFER_ (pk ), 0xc0 ); /* nil marker */
96+ }
97+
98+ /*
99+ * Check if a value was already serialized and return its ref_id if so.
100+ * If not found, registers the value and returns 0.
101+ */
102+ static long msgpack_packer_check_ref (msgpack_packer_t * pk , VALUE v )
103+ {
104+ if (!pk -> ref_table ) {
105+ pk -> ref_table = st_init_numtable ();
106+ }
107+
108+ st_data_t ref_id ;
109+ if (st_lookup (pk -> ref_table , (st_data_t )v , & ref_id )) {
110+ return (long )ref_id ;
111+ }
112+
113+ /* Not found - register this value */
114+ st_insert (pk -> ref_table , (st_data_t )v , (st_data_t )pk -> next_ref_id );
115+ pk -> next_ref_id ++ ;
116+ return 0 ; /* 0 means "not a back-reference" */
49117}
50118
51119
@@ -136,6 +204,18 @@ bool msgpack_packer_try_write_with_ext_type_lookup(msgpack_packer_t* pk, VALUE v
136204 return false;
137205 }
138206
207+ /* Handle ref_tracking: check if we've seen this object before */
208+ if (ext_flags & MSGPACK_EXT_REF_TRACKING ) {
209+ long ref_id = msgpack_packer_check_ref (pk , v );
210+ if (ref_id > 0 ) {
211+ /* Already seen - write back-reference */
212+ msgpack_packer_write_back_ref (pk , ref_id );
213+ return true;
214+ }
215+ /* Not seen before - write new-ref header, then continue to serialize normally */
216+ msgpack_packer_write_new_ref_header (pk );
217+ }
218+
139219 if (ext_flags & MSGPACK_EXT_STRUCT_FAST_PATH ) {
140220 /* Fast path for Struct: directly access fields in C, no Ruby callbacks */
141221 VALUE held_buffer = MessagePack_Buffer_hold (& pk -> buffer );
0 commit comments