leptris 1.9.199.0-arm-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3890 -0
  3. data/CLAUDE.md +115 -0
  4. data/CONTEXT.md +39 -0
  5. data/LICENSE.md +33 -0
  6. data/README.adoc +755 -0
  7. data/Rakefile +407 -0
  8. data/TODO.perf/01-ext-accelerated-default-reads.md +28 -0
  9. data/TODO.perf/02-auto-enable-native-layer.md +19 -0
  10. data/TODO.perf/03-bulk-xpath-result-materialization.md +18 -0
  11. data/TODO.perf/04-ext-bound-serialization.md +15 -0
  12. data/TODO.perf/05-native-surface-completion.md +19 -0
  13. data/TODO.perf/06-perf-battery-native-legs.md +16 -0
  14. data/TODO.perf/07-native-read-floor.md +31 -0
  15. data/TODO.perf/08-version-coherent-native-mutations.md +24 -0
  16. data/TODO.perf/09-adoption-lift-fast-path.md +38 -0
  17. data/TODO.perf/10-bulk-attribute-materialization.md +20 -0
  18. data/TODO.perf/11-moxml-gap-battery.md +18 -0
  19. data/TODO.perf/12-document-lifetime-in-c.md +47 -0
  20. data/TODO.perf/13-structural-memos.md +49 -0
  21. data/TODO.perf/14-insert-family-c-bound.md +31 -0
  22. data/TODO.perf/15-compiled-expression-cache.md +35 -0
  23. data/TODO.perf/16-at-xpath-single-result-seam.md +38 -0
  24. data/TODO.perf/17-precomputed-fast-path-flags.md +35 -0
  25. data/TODO.perf/18-inner-html-one-c-pass.md +32 -0
  26. data/TODO.perf/19-lazy-node-pointer.md +39 -0
  27. data/TODO.perf/20-css-translation-cache.md +24 -0
  28. data/TODO.perf/21-key-memo-consult.md +20 -0
  29. data/TODO.perf/22-eager-nodeset-materialization.md +30 -0
  30. data/TODO.perf/23-cbound-value-mutations.md +27 -0
  31. data/TODO.perf/24-immutable-read-lanes.md +22 -0
  32. data/TODO.perf/25-scope-owned-bulk-path.md +39 -0
  33. data/TODO.perf/26-post-mutation-memo-seeding.md +26 -0
  34. data/TODO.perf/27-c-yield-traversal.md +38 -0
  35. data/TODO.perf/28-address-fills.md +25 -0
  36. data/TODO.perf/29-ns-xpath-compiled.md +23 -0
  37. data/TODO.perf/30-copy-and-element-child-faces.md +31 -0
  38. data/TODO.perf/31-battery-rounds-4-8.md +23 -0
  39. data/TODO.perf/32-clean-host-floor-table.md +29 -0
  40. data/TODO.perf/33-cbound-root-set.md +29 -0
  41. data/TODO.perf/34-fragment-fast-lane.md +41 -0
  42. data/TODO.perf/35-parse-default-c-face.md +24 -0
  43. data/TODO.perf/36-raise-in-c-serializer-encoding.md +34 -0
  44. data/TODO.perf/37-document-lazy-pointer.md +28 -0
  45. data/TODO.perf/38-bulk-walk-and-bench-gate.md +23 -0
  46. data/TODO.restructure/01-constraint-compliance-audit.md +40 -0
  47. data/TODO.restructure/02-deep-copy-seam.md +21 -0
  48. data/TODO.restructure/03-evaluation-context-seam.md +18 -0
  49. data/TODO.restructure/04-spec-mece-restructure.md +20 -0
  50. data/TODO.restructure/05-memory-documentation.md +13 -0
  51. data/TODO.restructure/06-architecture-map-refresh.md +12 -0
  52. data/TODO.restructure/07-external-gates.md +16 -0
  53. data/TODO.restructure/08-new-code-audit-and-ext-memo.md +11 -0
  54. data/TODO.restructure/09-iteration-scope.md +29 -0
  55. data/TODO.restructure/10-cold-attr-read-diet.md +19 -0
  56. data/TODO.restructure/11-construction-factory-diet.md +16 -0
  57. data/TODO.restructure/12-resultattr-merge.md +12 -0
  58. data/TODO.restructure/13-spec-mece-followup.md +9 -0
  59. data/TODO.restructure/14-constraint-audit-refresh.md +15 -0
  60. data/TODO.restructure/15-readme-feature-documentation.md +10 -0
  61. data/TODO.restructure/16-claudemd-map-refresh.md +5 -0
  62. data/TODO.restructure/17-perf-drift-record.md +12 -0
  63. data/TODO.restructure/18-audit-spec-doubles-and-gem-contents.md +12 -0
  64. data/TODO.restructure/19-gitignore-hygiene.md +10 -0
  65. data/TODO.restructure/20-utf8proc-enablement.md +34 -0
  66. data/TODO.restructure/21-typeddata-variant-prototype.md +42 -0
  67. data/TODO.restructure/22-sax-drain-bulk-strip.md +28 -0
  68. data/docs/adr/0001-lockstep-mirror.md +17 -0
  69. data/docs/adr/0002-utf8-at-the-seam.md +19 -0
  70. data/docs/adr/0003-readonly-memoization-pattern.md +34 -0
  71. data/docs/adr/0004-lifetime-guard.md +21 -0
  72. data/docs/adr/0005-autoload-manifest-ordering.md +19 -0
  73. data/docs/adr/0006-ruby-variant-policy.md +25 -0
  74. data/ext/build_windows_native.rb +42 -0
  75. data/ext/leptris/native/extconf.rb +55 -0
  76. data/ext/leptris/native/native.c +2528 -0
  77. data/leptris.gemspec +43 -0
  78. data/lib/leptris/version.rb +5 -0
  79. data/lib/leptris/xml/attr.rb +76 -0
  80. data/lib/leptris/xml/c_string_array.rb +37 -0
  81. data/lib/leptris/xml/cdata.rb +27 -0
  82. data/lib/leptris/xml/comment.rb +27 -0
  83. data/lib/leptris/xml/css_to_xpath.rb +192 -0
  84. data/lib/leptris/xml/descriptor.rb +214 -0
  85. data/lib/leptris/xml/diff.rb +74 -0
  86. data/lib/leptris/xml/doc_type.rb +54 -0
  87. data/lib/leptris/xml/document.rb +716 -0
  88. data/lib/leptris/xml/document_fragment.rb +63 -0
  89. data/lib/leptris/xml/element.rb +771 -0
  90. data/lib/leptris/xml/entity_reference.rb +13 -0
  91. data/lib/leptris/xml/evaluation_context.rb +32 -0
  92. data/lib/leptris/xml/ffi.rb +1527 -0
  93. data/lib/leptris/xml/iteration_scope.rb +56 -0
  94. data/lib/leptris/xml/iterparse.rb +144 -0
  95. data/lib/leptris/xml/namespace.rb +43 -0
  96. data/lib/leptris/xml/native.so +0 -0
  97. data/lib/leptris/xml/native_layer.rb +92 -0
  98. data/lib/leptris/xml/node.rb +686 -0
  99. data/lib/leptris/xml/node_set.rb +212 -0
  100. data/lib/leptris/xml/parse_options.rb +97 -0
  101. data/lib/leptris/xml/plan_value.rb +121 -0
  102. data/lib/leptris/xml/processing_instruction.rb +90 -0
  103. data/lib/leptris/xml/pull.rb +212 -0
  104. data/lib/leptris/xml/relaxng.rb +126 -0
  105. data/lib/leptris/xml/result_attr.rb +39 -0
  106. data/lib/leptris/xml/result_text.rb +36 -0
  107. data/lib/leptris/xml/sax/document.rb +45 -0
  108. data/lib/leptris/xml/sax/dom_dispatch.rb +144 -0
  109. data/lib/leptris/xml/sax/parser.rb +326 -0
  110. data/lib/leptris/xml/sax/recorder.rb +374 -0
  111. data/lib/leptris/xml/sax.rb +14 -0
  112. data/lib/leptris/xml/schematron.rb +88 -0
  113. data/lib/leptris/xml/searchable.rb +361 -0
  114. data/lib/leptris/xml/serialization.rb +180 -0
  115. data/lib/leptris/xml/text.rb +32 -0
  116. data/lib/leptris/xml/xpath.rb +89 -0
  117. data/lib/leptris/xml/xquery.rb +77 -0
  118. data/lib/leptris/xml/xslt.rb +85 -0
  119. data/lib/leptris/xml.rb +108 -0
  120. data/lib/leptris.rb +53 -0
  121. data/lib/libleptris.so +0 -0
  122. data/lib/libutf8proc.so.3 +0 -0
  123. data/scripts/gem_smoke.rb +36 -0
  124. metadata +214 -0
@@ -0,0 +1,2528 @@
1
+ /* Native TypedData node prototype (#185/#147/#187 converged):
2
+ * wrapper objects with the C pointer embedded (no FFI::Pointer per
3
+ * node), hot reads (name/content/attribute) called directly in C
4
+ * via dlsym on the FFI-loaded library, and children constructed in
5
+ * bulk — one cache round-trip, zero Ruby frames, zero FFI
6
+ * marshaling per node. */
7
+ #include <ruby.h>
8
+ #include <ruby/encoding.h>
9
+ #include <stdint.h>
10
+ #include <string.h>
11
+
12
+ #ifdef _WIN32
13
+ #include <windows.h>
14
+ #else
15
+ #include <dlfcn.h>
16
+ #endif
17
+
18
+ typedef const char *(*elem_name_fn)(void *);
19
+ typedef const char *(*text_content_fn)(void *);
20
+ typedef const char *(*attr_fn)(void *, const char *);
21
+ typedef size_t (*children_ex_fn)(void *, void **, int *, size_t);
22
+ typedef int (*node_type_fn)(void *);
23
+ typedef void *(*next_sibling_fn)(void *);
24
+ typedef void *(*parent_fn)(void *);
25
+ typedef const char *(*elem_prefix_fn)(void *);
26
+ typedef const char *(*elem_ns_fn)(void *);
27
+ typedef size_t (*xp_count_fn)(void *);
28
+ typedef size_t (*xp_nodes_ex_fn)(void *, void **, int *, size_t);
29
+ typedef int (*xp_node_kind_fn)(void *, int);
30
+ typedef const char *(*xp_node_name_fn)(void *, int);
31
+ typedef const char *(*xp_node_value_fn)(void *, int);
32
+ typedef size_t (*serialize_into_fn)(void *, char *, size_t, void *, void *);
33
+ typedef int (*ns_count_fn)(void *);
34
+ typedef void *(*attr_first_fn)(void *);
35
+ typedef void *(*attr_next_fn)(void *);
36
+ typedef const char *(*attr_name_fn)(void *);
37
+ typedef const char *(*attr_value_fn)(void *, void *);
38
+ typedef unsigned int (*node_line_fn)(void *);
39
+ typedef size_t (*node_offset_fn)(void *);
40
+ typedef const char *(*element_text_fn)(void *);
41
+ typedef void *(*doc_create_fn)(void);
42
+ typedef void *(*elem_create_fn)(void *, const char *);
43
+ typedef void *(*text_create_fn)(void *, const char *);
44
+ typedef void *(*create_child_fn)(void *, const char *);
45
+ typedef int (*append_child_fn)(void *, void *);
46
+ typedef int (*set_attr_fn)(void *, const char *, const char *);
47
+ typedef int (*set_root_fn)(void *, void *);
48
+ typedef void (*doc_free_fn)(void *);
49
+
50
+ static elem_name_fn f_elem_name;
51
+ static text_content_fn f_text_content;
52
+ static attr_fn f_attr;
53
+ static children_ex_fn f_children_ex;
54
+ static node_type_fn f_node_type;
55
+ static next_sibling_fn f_next_sibling;
56
+ static parent_fn f_parent;
57
+ static elem_prefix_fn f_elem_prefix;
58
+ static elem_ns_fn f_elem_ns;
59
+ static xp_count_fn f_xp_count;
60
+ static xp_nodes_ex_fn f_xp_nodes_ex;
61
+ static xp_node_kind_fn f_xp_node_kind;
62
+ static xp_node_name_fn f_xp_node_name;
63
+ static xp_node_value_fn f_xp_node_value;
64
+ static serialize_into_fn f_doc_serialize, f_elem_serialize;
65
+ static ns_count_fn f_elem_ns_count;
66
+ static attr_first_fn f_attr_first;
67
+ static attr_next_fn f_attr_next;
68
+ static attr_name_fn f_attr_name;
69
+ static attr_value_fn f_attr_value;
70
+ static node_line_fn f_node_line;
71
+ static node_offset_fn f_node_offset;
72
+ static element_text_fn f_element_text;
73
+ static doc_create_fn f_doc_create;
74
+ static elem_create_fn f_elem_create;
75
+ static text_create_fn f_text_create;
76
+ static create_child_fn f_create_child;
77
+ static append_child_fn f_append_child;
78
+ static append_child_fn f_prepend_child, f_insert_after, f_insert_before;
79
+ static set_attr_fn f_set_attr;
80
+ typedef int (*xp_type_fn)(void *);
81
+ typedef void (*xp_free_fn)(void *);
82
+ typedef void *(*first_child_fn)(void *);
83
+ typedef const char *(*node_str_fn)(void *);
84
+ static xp_type_fn f_xp_type;
85
+ static xp_free_fn f_xp_free;
86
+ static first_child_fn f_first_child;
87
+ static node_str_fn f_comment_content, f_cdata_content, f_pi_target,
88
+ f_pi_data;
89
+ typedef int (*set_str_fn)(void *, const char *);
90
+ typedef int (*node_unlink_fn)(void *);
91
+ typedef int (*traverse_cb_fn)(void *, void *);
92
+ typedef int (*traverse_fn)(void *, int, traverse_cb_fn, void *);
93
+ typedef void (*visit_cb_fn)(void *, void *, int, int);
94
+ typedef void (*visit_fn)(void *, visit_cb_fn, void *);
95
+ typedef void (*free_str_fn)(void *);
96
+ typedef const char *(*node_xpath_fn)(void *);
97
+ typedef void *(*elem_copy_fn)(void *, void *);
98
+ typedef void *(*parse_str_fn)(const char *, size_t, void *);
99
+ typedef void *(*parse_frag_fn)(const char *, size_t, void *, int *);
100
+ typedef const char *(*last_err_fn)(void);
101
+ typedef const char *(*status_str_fn)(int);
102
+ static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
103
+ static node_unlink_fn f_node_unlink;
104
+ static traverse_fn f_node_traverse;
105
+ static visit_fn f_node_visit;
106
+ static free_str_fn f_free_str;
107
+ static node_xpath_fn f_node_xpath;
108
+ static elem_copy_fn f_elem_copy;
109
+ static parse_str_fn f_parse_str;
110
+ static parse_frag_fn f_parse_frag;
111
+ static last_err_fn f_last_err;
112
+ static status_str_fn f_status_str;
113
+ static set_root_fn f_set_root;
114
+ static doc_free_fn f_doc_free;
115
+
116
+ static VALUE c_native_node;
117
+ /* Ivar reads (TODO.perf/07 tail): the binding Document keeps the
118
+ * C address and the caches in plain ivars, so the hot C faces skip
119
+ * rb_funcall dispatch entirely. */
120
+ static ID id_iv_c_address, id_iv_native_cache, id_iv_binding_cache;
121
+ static ID id_iv_version, id_iv_readonly, id_address;
122
+ static ID id_iv_nn_content, id_iv_nn_content_ver;
123
+ static ID id_iv_nn_attrs, id_iv_nn_attrs_ver;
124
+ static ID id_child_klasses;
125
+ static VALUE c_use_after_free_error;
126
+
127
+ static VALUE native_cache_of(VALUE document);
128
+ static void resolve_binding_classes(void);
129
+ static VALUE binding_cache_of(VALUE document);
130
+ static VALUE binding_klass_for(int kind);
131
+ static VALUE c_iteration_scope;
132
+ static VALUE c_leptris_error;
133
+ static VALUE c_b_document, c_b_freed, c_ffi_pointer;
134
+ static ID id_ptr_new, id_freed_new, id_alive;
135
+ static void check_status_c(int st);
136
+
137
+ #define NT_ELEMENT 0
138
+
139
+ struct native_node {
140
+ void *ptr;
141
+ VALUE document;
142
+ /* Single-slot read memos (#204 ask 1): the last attribute
143
+ * query and the last content, stamped with the document's
144
+ * mutation version (a Fixnum immediate — pointer compare).
145
+ * Slot beats the hash memo on repeat reads of one name. */
146
+ VALUE attr_key, attr_val, attr_ver;
147
+ VALUE content_val, content_ver;
148
+ };
149
+
150
+ static void nn_mark(void *p)
151
+ {
152
+ struct native_node *n = p;
153
+ if (n->document != Qnil)
154
+ rb_gc_mark(n->document);
155
+ if (n->attr_key != Qnil)
156
+ rb_gc_mark(n->attr_key);
157
+ if (n->attr_val != Qnil)
158
+ rb_gc_mark(n->attr_val);
159
+ if (n->content_val != Qnil)
160
+ rb_gc_mark(n->content_val);
161
+ }
162
+
163
+ static size_t nn_size(const void *p) { (void)p; return sizeof(struct native_node); }
164
+
165
+ static const rb_data_type_t nn_type = {
166
+ "Leptris/XML/NativeNode",
167
+ { nn_mark, RUBY_TYPED_DEFAULT_FREE, nn_size, },
168
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
169
+ };
170
+
171
+ #ifdef _WIN32
172
+ static void *lib_open(const char *path) { return (void *)LoadLibraryA(path); }
173
+ static void *lib_sym(void *h, const char *name)
174
+ {
175
+ return (void *)GetProcAddress((HMODULE)h, name);
176
+ }
177
+ #else
178
+ static void *lib_open(const char *path) { return dlopen(path, RTLD_NOW); }
179
+ static void *lib_sym(void *h, const char *name) { return dlsym(h, name); }
180
+ #endif
181
+
182
+ static void resolve_symbols(const char *lib_path)
183
+ {
184
+ void *h = lib_open(lib_path);
185
+ if (!h)
186
+ rb_raise(rb_eRuntimeError, "cannot load library %s", lib_path);
187
+ f_elem_name = (elem_name_fn)lib_sym(h, "leptris_element_name");
188
+ f_text_content = (text_content_fn)lib_sym(h, "leptris_text_node_get_content");
189
+ f_attr = (attr_fn)lib_sym(h, "leptris_element_attribute");
190
+ f_children_ex = (children_ex_fn)lib_sym(h, "leptris_node_children_ex");
191
+ f_node_type = (node_type_fn)lib_sym(h, "leptris_node_get_type");
192
+ f_next_sibling = (next_sibling_fn)lib_sym(h, "leptris_node_next_sibling");
193
+ f_parent = (parent_fn)lib_sym(h, "leptris_node_parent");
194
+ f_elem_prefix = (elem_prefix_fn)lib_sym(h, "leptris_element_prefix");
195
+ f_elem_ns_count = (ns_count_fn)lib_sym(h, "leptris_element_namespace_count");
196
+ f_elem_ns = (elem_ns_fn)lib_sym(h, "leptris_element_namespace");
197
+ f_xp_count = (xp_count_fn)lib_sym(h, "leptris_xpath_result_count");
198
+ f_xp_nodes_ex = (xp_nodes_ex_fn)lib_sym(h, "leptris_xpath_result_get_nodes_ex");
199
+ f_xp_node_kind = (xp_node_kind_fn)lib_sym(h, "leptris_xpath_result_node_kind");
200
+ f_xp_node_name = (xp_node_name_fn)lib_sym(h, "leptris_xpath_result_node_name");
201
+ f_xp_node_value = (xp_node_value_fn)lib_sym(h, "leptris_xpath_result_node_value");
202
+ f_doc_serialize = (serialize_into_fn)lib_sym(h, "leptris_document_serialize_into");
203
+ f_elem_serialize = (serialize_into_fn)lib_sym(h, "leptris_element_serialize_into");
204
+ f_attr_first = (attr_first_fn)lib_sym(h, "leptris_element_first_attribute");
205
+ f_attr_next = (attr_next_fn)lib_sym(h, "leptris_attribute_next");
206
+ f_attr_name = (attr_name_fn)lib_sym(h, "leptris_attribute_get_name");
207
+ f_attr_value = (attr_value_fn)lib_sym(h, "leptris_attribute_get_value");
208
+ f_node_line = (node_line_fn)lib_sym(h, "leptris_node_line");
209
+ f_node_offset = (node_offset_fn)lib_sym(h, "leptris_node_byte_offset");
210
+ f_element_text = (element_text_fn)lib_sym(h, "leptris_element_text");
211
+ f_doc_create = (doc_create_fn)lib_sym(h, "leptris_document_create");
212
+ f_elem_create = (elem_create_fn)lib_sym(h, "leptris_element_create");
213
+ f_text_create = (text_create_fn)lib_sym(h, "leptris_text_node_create");
214
+ f_create_child = (create_child_fn)lib_sym(h, "leptris_element_create_child");
215
+ f_append_child = (append_child_fn)lib_sym(h, "leptris_element_append_child");
216
+ f_prepend_child = (append_child_fn)lib_sym(h, "leptris_element_prepend_child");
217
+ f_insert_after = (append_child_fn)lib_sym(h, "leptris_element_insert_after");
218
+ f_insert_before = (append_child_fn)lib_sym(h, "leptris_element_insert_before");
219
+ f_set_attr = (set_attr_fn)lib_sym(h, "leptris_element_set_attribute");
220
+ f_xp_type = (xp_type_fn)lib_sym(h, "leptris_xpath_result_type");
221
+ f_xp_free = (xp_free_fn)lib_sym(h, "leptris_xpath_result_free");
222
+ f_first_child = (first_child_fn)lib_sym(h, "leptris_node_first_child");
223
+ f_comment_content = (node_str_fn)lib_sym(h, "leptris_comment_node_get_content");
224
+ f_cdata_content = (node_str_fn)lib_sym(h, "leptris_cdata_node_get_content");
225
+ f_pi_target = (node_str_fn)lib_sym(h, "leptris_pi_node_get_target");
226
+ f_pi_data = (node_str_fn)lib_sym(h, "leptris_pi_node_get_data");
227
+ f_elem_set_name = (set_str_fn)lib_sym(h, "leptris_element_set_name");
228
+ f_elem_set_text = (set_str_fn)lib_sym(h, "leptris_element_set_text");
229
+ f_text_set_content = (set_str_fn)lib_sym(h, "leptris_text_node_set_content");
230
+ f_node_unlink = (node_unlink_fn)lib_sym(h, "leptris_node_unlink");
231
+ f_node_traverse = (traverse_fn)lib_sym(h, "leptris_node_traverse");
232
+ f_node_visit = (visit_fn)lib_sym(h, "leptris_node_visit");
233
+ f_free_str = (free_str_fn)lib_sym(h, "leptris_free_string");
234
+ f_node_xpath = (node_xpath_fn)lib_sym(h, "leptris_node_get_xpath");
235
+ f_elem_copy = (elem_copy_fn)lib_sym(h, "leptris_element_copy");
236
+ f_parse_str = (parse_str_fn)lib_sym(h, "leptris_parse_string");
237
+ f_parse_frag = (parse_frag_fn)lib_sym(h, "leptris_parse_fragment");
238
+ f_last_err = (last_err_fn)lib_sym(h, "leptris_last_error");
239
+ f_status_str = (status_str_fn)lib_sym(h, "leptris_status_string");
240
+ f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
241
+ f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
242
+ if (!f_elem_name || !f_text_content || !f_attr ||
243
+ !f_children_ex || !f_node_type || !f_next_sibling || !f_parent ||
244
+ !f_element_text || !f_doc_create || !f_elem_create || !f_text_create ||
245
+ !f_create_child || !f_append_child || !f_prepend_child ||
246
+ !f_insert_after || !f_insert_before || !f_set_attr || !f_set_root ||
247
+ !f_xp_type || !f_xp_free || !f_first_child ||
248
+ !f_comment_content || !f_cdata_content || !f_pi_target ||
249
+ !f_pi_data || !f_elem_set_name || !f_elem_set_text ||
250
+ !f_text_set_content || !f_node_unlink ||
251
+ !f_node_traverse || !f_node_visit || !f_free_str ||
252
+ !f_node_xpath || !f_elem_copy || !f_parse_str ||
253
+ !f_parse_frag || !f_last_err || !f_status_str ||
254
+ !f_doc_free ||
255
+ !f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
256
+ !f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
257
+ !f_doc_serialize || !f_elem_serialize || !f_attr_first ||
258
+ !f_elem_ns_count || !f_elem_ns ||
259
+ !f_attr_next || !f_attr_name || !f_attr_value ||
260
+ !f_node_line || !f_node_offset)
261
+ rb_raise(rb_eRuntimeError, "libleptris symbols missing");
262
+ }
263
+
264
+ static VALUE nn_name(VALUE self)
265
+ {
266
+ struct native_node *n;
267
+ const char *s;
268
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
269
+ s = f_elem_name(n->ptr);
270
+ /* Interned (shared, frozen) fstring: element names repeat
271
+ * massively across a document — one RString per distinct name
272
+ * per process instead of one per read (lutaml/moxml#249:
273
+ * ~18% of consumer materialize allocations were fresh name
274
+ * strings). Names are identifiers; nothing downstream mutates
275
+ * them in place (renames mint new strings). */
276
+ return s ? rb_enc_interned_str(s, strlen(s), rb_utf8_encoding()) : Qnil;
277
+ }
278
+
279
+ static VALUE nn_content(VALUE self)
280
+ {
281
+ struct native_node *n;
282
+ const char *s;
283
+ VALUE doc, cached;
284
+
285
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
286
+ /* Version-stamped content memo (the binding Document's
287
+ * mutation version — the same invalidation discipline as
288
+ * binding memos, ADR 0003): repeat reads are two struct
289
+ * reads and a pointer compare. nil results never cache
290
+ * (empty elements recompute — rare in hot walks). */
291
+ doc = n->document;
292
+ if (doc != Qnil && n->content_ver != Qnil &&
293
+ n->content_ver == rb_ivar_get(doc, id_iv_version))
294
+ return n->content_val;
295
+ /* elements aggregate their text (leptris_element_text); text
296
+ * nodes carry it directly. */
297
+ s = f_node_type(n->ptr) == 0 ? f_element_text(n->ptr)
298
+ : f_text_content(n->ptr);
299
+ cached = s ? rb_utf8_str_new_cstr(s) : Qnil;
300
+ if (doc != Qnil && cached != Qnil) {
301
+ n->content_val = cached;
302
+ n->content_ver = rb_ivar_get(doc, id_iv_version);
303
+ }
304
+ return cached;
305
+ }
306
+
307
+ static VALUE nf_fast_attribute2(VALUE self, VALUE addr, VALUE name)
308
+ {
309
+ const char *s;
310
+ (void)self;
311
+ s = f_attr((void *)(uintptr_t)NUM2ULL(addr),
312
+ RSTRING_PTR(StringValue(name)));
313
+ return s ? rb_utf8_str_new_cstr(s) : Qnil;
314
+ }
315
+
316
+ /* 07 (#204 ask 1): StringValueCStr runs a memchr over the name
317
+ * checking for embedded NULs — attribute names cannot contain
318
+ * NULs, so StringValue + RSTRING_PTR skips the scan. */
319
+ static VALUE nn_attribute(VALUE self, VALUE name)
320
+ {
321
+ struct native_node *n;
322
+ VALUE doc, cache, v;
323
+ const char *s;
324
+
325
+ /* String-normalized once, used as both the C argument and the
326
+ * cache key. */
327
+ if (!RB_TYPE_P(name, T_STRING))
328
+ name = rb_obj_as_string(name);
329
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
330
+ doc = n->document;
331
+ /* Single-slot memo first: the last-queried name by VALUE
332
+ * (callers may mint a fresh String per call) against the same
333
+ * version. Then the hash memo for other repeat names; both
334
+ * stamp with the document version, so any mutation through
335
+ * either surface drops them. Absent names never cache (no
336
+ * negative-cache staleness). */
337
+ if (doc != Qnil && n->attr_ver == rb_ivar_get(doc, id_iv_version) &&
338
+ n->attr_key != Qnil && rb_str_equal(n->attr_key, name))
339
+ return n->attr_val;
340
+ cache = Qnil;
341
+ if (doc != Qnil) {
342
+ cache = rb_ivar_get(self, id_iv_nn_attrs);
343
+ if (cache != Qnil &&
344
+ rb_ivar_get(self, id_iv_nn_attrs_ver) ==
345
+ rb_ivar_get(doc, id_iv_version)) {
346
+ v = rb_hash_aref(cache, name);
347
+ if (v != Qnil) {
348
+ n->attr_key = name;
349
+ n->attr_val = v;
350
+ n->attr_ver = rb_ivar_get(doc, id_iv_version);
351
+ return v;
352
+ }
353
+ }
354
+ }
355
+ s = f_attr(n->ptr, RSTRING_PTR(name));
356
+ v = s ? rb_utf8_str_new_cstr(s) : Qnil;
357
+ if (doc != Qnil && v != Qnil) {
358
+ if (cache == Qnil)
359
+ cache = rb_hash_new();
360
+ rb_hash_aset(cache, name, v);
361
+ rb_ivar_set(self, id_iv_nn_attrs, cache);
362
+ rb_ivar_set(self, id_iv_nn_attrs_ver,
363
+ rb_ivar_get(doc, id_iv_version));
364
+ n->attr_key = name;
365
+ n->attr_val = v;
366
+ n->attr_ver = rb_ivar_get(doc, id_iv_version);
367
+ }
368
+ return v;
369
+ }
370
+
371
+ static VALUE nn_allocate(VALUE klass)
372
+ {
373
+ struct native_node *n;
374
+ return TypedData_Make_Struct(klass, struct native_node, &nn_type, n);
375
+ }
376
+
377
+ /* Klass-propagating reads (#246): a consumer subclass installed a
378
+ * per-kind child-klass map; reads mint that class so the child
379
+ * arrives already being the consumer's wrapper. Map-less callers
380
+ * (the base class) keep today's base mint. */
381
+ static VALUE klass_for_kind(VALUE owner_klass, int kind)
382
+ {
383
+ VALUE map = rb_attr_get(owner_klass, id_child_klasses);
384
+ VALUE k;
385
+ if (NIL_P(map))
386
+ return c_native_node;
387
+ k = rb_ary_entry(map, (long)kind);
388
+ return RB_TYPE_P(k, T_CLASS) ? k : c_native_node;
389
+ }
390
+
391
+ static VALUE nn_install_child_klasses(VALUE self, VALUE map)
392
+ {
393
+ long i, len;
394
+ Check_Type(map, T_ARRAY);
395
+ len = RARRAY_LEN(map);
396
+ if (len > 5)
397
+ rb_raise(rb_eArgError,
398
+ "child klasses: Array of classes/nil indexed by "
399
+ "node kind (element, text, comment, cdata, pi)");
400
+ for (i = 0; i < len; i++) {
401
+ VALUE k = rb_ary_entry(map, i);
402
+ if (!NIL_P(k) &&
403
+ !(RB_TYPE_P(k, T_CLASS) &&
404
+ RTEST(rb_class_inherited_p(k, c_native_node))))
405
+ rb_raise(rb_eTypeError,
406
+ "child klasses entries must be nil or a "
407
+ "Leptris::XML::NativeNode subclass");
408
+ }
409
+ rb_ivar_set(self, id_child_klasses, map);
410
+ return self;
411
+ }
412
+
413
+ /* Bulk children: one native pass; cache check/store inline; the
414
+ * document's wrapper_cache (a Ruby Hash keyed by address) provides
415
+ * identity for nodes seen through other paths. */
416
+ /* Two-call children fetch (#202): children_ex copies
417
+ * min(total, cap); a saturated fill means the list may be longer —
418
+ * query the true count (buf NULL) and refetch grown. */
419
+ static int fetch_children_two_call(void *node, void ***buf_out,
420
+ int **kinds_out)
421
+ {
422
+ size_t cap = 512;
423
+ void *b = ruby_xmalloc(cap * sizeof(void *));
424
+ int *k = ruby_xmalloc((int)cap * sizeof(int));
425
+ size_t count = f_children_ex(node, b, k, cap);
426
+ if (count == cap) {
427
+ size_t total = f_children_ex(node, NULL, NULL, 0);
428
+ if (total > cap) {
429
+ ruby_xfree(b);
430
+ ruby_xfree(k);
431
+ cap = total;
432
+ b = ruby_xmalloc(cap * sizeof(void *));
433
+ k = ruby_xmalloc((int)cap * sizeof(int));
434
+ count = f_children_ex(node, b, k, cap);
435
+ }
436
+ }
437
+ *buf_out = b;
438
+ *kinds_out = k;
439
+ return (int)count;
440
+ }
441
+
442
+ static VALUE nn_children(VALUE self)
443
+ {
444
+ struct native_node *n;
445
+ void **buf;
446
+ int *kinds;
447
+ int count, i;
448
+ VALUE cache, out, owner_klass;
449
+
450
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
451
+ count = fetch_children_two_call(n->ptr, &buf, &kinds);
452
+ if (count <= 0) {
453
+ ruby_xfree(buf);
454
+ ruby_xfree(kinds);
455
+ return rb_ary_new2(0);
456
+ }
457
+
458
+ cache = native_cache_of(n->document);
459
+ out = rb_ary_new2(count);
460
+ owner_klass = rb_obj_class(self);
461
+ for (i = 0; i < count; i++) {
462
+ uint64_t addr = (uint64_t)(uintptr_t)buf[i];
463
+ VALUE key = ULL2NUM(addr);
464
+ VALUE child = rb_hash_aref(cache, key);
465
+ if (NIL_P(child)) {
466
+ struct native_node *cn;
467
+ child = nn_allocate(klass_for_kind(owner_klass, kinds[i]));
468
+ TypedData_Get_Struct(child, struct native_node, &nn_type, cn);
469
+ cn->ptr = buf[i];
470
+ cn->document = n->document;
471
+ rb_hash_aset(cache, key, child);
472
+ }
473
+ rb_ary_push(out, child);
474
+ }
475
+ ruby_xfree(buf);
476
+ ruby_xfree(kinds);
477
+ return out;
478
+ }
479
+
480
+ static VALUE nn_type_sym(VALUE self)
481
+ {
482
+ struct native_node *n;
483
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
484
+ switch (f_node_type(n->ptr)) {
485
+ case 0: return ID2SYM(rb_intern("element"));
486
+ case 1: return ID2SYM(rb_intern("text"));
487
+ case 2: return ID2SYM(rb_intern("comment"));
488
+ case 3: return ID2SYM(rb_intern("cdata"));
489
+ case 4: return ID2SYM(rb_intern("pi"));
490
+ default: return ID2SYM(rb_intern("node"));
491
+ }
492
+ }
493
+
494
+ /* Bulk children filtered to elements — the walk shape consumers
495
+ * actually iterate (#185's element_children parity). */
496
+ static VALUE nn_element_children(VALUE self)
497
+ {
498
+ struct native_node *n;
499
+ void **buf;
500
+ int *kinds;
501
+ int count, i;
502
+ VALUE cache, out, child_klass;
503
+
504
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
505
+ count = fetch_children_two_call(n->ptr, &buf, &kinds);
506
+ if (count <= 0) {
507
+ ruby_xfree(buf);
508
+ ruby_xfree(kinds);
509
+ return rb_ary_new2(0);
510
+ }
511
+
512
+ cache = native_cache_of(n->document);
513
+ out = rb_ary_new();
514
+ child_klass = klass_for_kind(rb_obj_class(self), NT_ELEMENT);
515
+ for (i = 0; i < count; i++) {
516
+ if (kinds[i] != NT_ELEMENT)
517
+ continue;
518
+ VALUE key = ULL2NUM((uint64_t)(uintptr_t)buf[i]);
519
+ VALUE child = rb_hash_aref(cache, key);
520
+ if (NIL_P(child)) {
521
+ struct native_node *cn;
522
+ child = nn_allocate(child_klass);
523
+ TypedData_Get_Struct(child, struct native_node, &nn_type, cn);
524
+ cn->ptr = buf[i];
525
+ cn->document = n->document;
526
+ rb_hash_aset(cache, key, child);
527
+ }
528
+ rb_ary_push(out, child);
529
+ }
530
+ ruby_xfree(buf);
531
+ ruby_xfree(kinds);
532
+ return out;
533
+ }
534
+
535
+ static VALUE wrap_cached(VALUE owner_klass, struct native_node *owner,
536
+ void *ptr)
537
+ {
538
+ VALUE cache, key, node;
539
+ struct native_node *n;
540
+
541
+ cache = native_cache_of(owner->document);
542
+ key = ULL2NUM((uint64_t)(uintptr_t)ptr);
543
+ node = rb_hash_aref(cache, key);
544
+ if (!NIL_P(node))
545
+ return node;
546
+ node = nn_allocate(klass_for_kind(owner_klass, f_node_type(ptr)));
547
+ TypedData_Get_Struct(node, struct native_node, &nn_type, n);
548
+ n->ptr = ptr;
549
+ n->document = owner->document;
550
+ rb_hash_aset(cache, key, node);
551
+ return node;
552
+ }
553
+
554
+ static VALUE nn_next_sibling(VALUE self)
555
+ {
556
+ struct native_node *n;
557
+ void *sib;
558
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
559
+ sib = f_next_sibling(n->ptr);
560
+ return sib ? wrap_cached(rb_obj_class(self), n, sib) : Qnil;
561
+ }
562
+
563
+ static VALUE nn_parent(VALUE self)
564
+ {
565
+ struct native_node *n;
566
+ void *par;
567
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
568
+ par = f_parent(n->ptr);
569
+ return par ? wrap_cached(rb_obj_class(self), n, par) : Qnil;
570
+ }
571
+
572
+ /* Wrap a freshly-created C node as a NativeNode and register it
573
+ * in the document's identity cache. */
574
+ static VALUE wrap_new(VALUE document, void *ptr)
575
+ {
576
+ struct native_node *n;
577
+ VALUE node, cache, key;
578
+
579
+ node = nn_allocate(c_native_node);
580
+ TypedData_Get_Struct(node, struct native_node, &nn_type, n);
581
+ n->ptr = ptr;
582
+ n->document = document;
583
+ cache = native_cache_of(document);
584
+ key = ULL2NUM((uint64_t)(uintptr_t)ptr);
585
+ rb_hash_aset(cache, key, node);
586
+ return node;
587
+ }
588
+
589
+ /* Document address from the binding Document's @c_address ivar
590
+ * (nil once freed) — one ivar read, no method dispatch. */
591
+ static void *doc_ptr_of(VALUE document)
592
+ {
593
+ VALUE addr = rb_ivar_get(document, id_iv_c_address);
594
+ if (NIL_P(addr))
595
+ rb_raise(rb_eRuntimeError, "document has been freed");
596
+ return (void *)(uintptr_t)NUM2ULL(addr);
597
+ }
598
+
599
+ /* Per-document identity caches. Both are lazily allocated on the
600
+ * Ruby side (@x ||= {}), so replicate that here: ivar read, create
601
+ * and store when unset. */
602
+ static VALUE native_cache_of(VALUE document)
603
+ {
604
+ VALUE cache = rb_ivar_get(document, id_iv_native_cache);
605
+ if (NIL_P(cache)) {
606
+ cache = rb_hash_new();
607
+ rb_ivar_set(document, id_iv_native_cache, cache);
608
+ }
609
+ return cache;
610
+ }
611
+
612
+ static VALUE binding_cache_of(VALUE document)
613
+ {
614
+ VALUE cache = rb_ivar_get(document, id_iv_binding_cache);
615
+ if (NIL_P(cache)) {
616
+ cache = rb_hash_new();
617
+ rb_ivar_set(document, id_iv_binding_cache, cache);
618
+ }
619
+ return cache;
620
+ }
621
+
622
+
623
+ /* TODO.perf/08 (#204 ask 2): native mutations must advance the
624
+ * binding Document's mutation version so binding memos invalidate
625
+ * exactly as they do through the FFI write path — and respect the
626
+ * readonly gate. Two ivar reads per mutation (the version is
627
+ * always a Fixnum, readonly is Qfalse/Qtrue). */
628
+ static VALUE c_readonly_error;
629
+
630
+ static void document_advance_version(VALUE document)
631
+ {
632
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
633
+ rb_raise(c_readonly_error,
634
+ "document is readonly — native mutation attempted");
635
+ rb_ivar_set(document, id_iv_version,
636
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
637
+ }
638
+
639
+ static VALUE nf_ns_lift_needed(VALUE self, VALUE addr)
640
+ {
641
+ void *node = (void *)(uintptr_t)NUM2ULL(addr);
642
+ const char *uri;
643
+ (void)self;
644
+ /* Exact semantics in two C calls: an element with NO resolved
645
+ * namespace and NO own declarations cannot lose anything in a
646
+ * move — bare name, no prefix binding, no default-ns
647
+ * dependency, nothing to prune. Everything else takes the full
648
+ * lift path (carries in-scope declarations, prunes redundant
649
+ * own ones). */
650
+ if (f_elem_ns_count(node) > 0)
651
+ return Qtrue;
652
+ uri = f_elem_ns(node);
653
+ return (uri && *uri) ? Qtrue : Qfalse;
654
+ }
655
+
656
+ /* ---- C-bound append (TODO.perf/08-09, #204 ask 3) --------------
657
+ * One dispatch for the common programmatic-build adoption: the
658
+ * readonly + liveness gates, the provable no-op namespace-lift
659
+ * predicate, the version bump, and the engine append. Returns
660
+ * the engine status int; Qnil means the child DOES need the
661
+ * namespace lift — the caller falls back to the Ruby path. */
662
+ static VALUE nf_append_binding_child(VALUE self, VALUE document,
663
+ VALUE parent_addr, VALUE child_addr)
664
+ {
665
+ void *parent, *child;
666
+ const char *uri;
667
+ int st;
668
+
669
+ (void)self;
670
+ resolve_binding_classes();
671
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
672
+ rb_raise(c_use_after_free_error,
673
+ "owning document has been freed");
674
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
675
+ rb_raise(c_readonly_error,
676
+ "document is readonly — mutation attempted");
677
+ parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
678
+ child = (void *)(uintptr_t)NUM2ULL(child_addr);
679
+ /* Same provable no-op as Element.skip_adoption_lift?: an
680
+ * element with no resolved namespace and no own declarations
681
+ * cannot lose anything in a move; non-elements can never need
682
+ * a lift. */
683
+ if (f_node_type(child) == NT_ELEMENT) {
684
+ if (f_elem_ns_count(child) > 0)
685
+ return Qnil;
686
+ uri = f_elem_ns(child);
687
+ if (uri && *uri)
688
+ return Qnil;
689
+ }
690
+ /* Bump before the call, mirroring ensure_writable!: a failed
691
+ * mutation merely discards memos. */
692
+ rb_ivar_set(document, id_iv_version,
693
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
694
+ st = f_append_child(parent, child);
695
+ check_status_c(st);
696
+ return Qtrue;
697
+ }
698
+
699
+ /* C-bound set_attribute (TODO.perf/11, #204 gap row 0.25x): the
700
+ * gates, the version bump (which drops the version-stamped
701
+ * attribute memos on both surfaces), and the engine write in one
702
+ * dispatch. */
703
+ static VALUE nf_set_binding_attribute(VALUE self, VALUE document,
704
+ VALUE addr, VALUE name, VALUE value)
705
+ {
706
+ void *node;
707
+ int st;
708
+
709
+ (void)self;
710
+ resolve_binding_classes();
711
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
712
+ rb_raise(c_use_after_free_error,
713
+ "owning document has been freed");
714
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
715
+ rb_raise(c_readonly_error,
716
+ "document is readonly — mutation attempted");
717
+ node = (void *)(uintptr_t)NUM2ULL(addr);
718
+ rb_ivar_set(document, id_iv_version,
719
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
720
+ /* Attribute names cannot contain NULs (StringValueCStr's
721
+ * memchr is a wasted scan); VALUES keep the scan so an
722
+ * embedded-NUL value raises rather than truncates. */
723
+ st = f_set_attr(node, RSTRING_PTR(StringValue(name)),
724
+ StringValueCStr(value));
725
+ check_status_c(st);
726
+ return Qtrue;
727
+ }
728
+
729
+ /* C-bound value mutations (TODO.perf/23): the same gates + bump
730
+ * + engine write shape as set_binding_attribute. */
731
+ static VALUE nf_set_binding_name(VALUE self, VALUE document,
732
+ VALUE addr, VALUE name)
733
+ {
734
+ void *node;
735
+ int st;
736
+
737
+ (void)self;
738
+ resolve_binding_classes();
739
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
740
+ rb_raise(c_use_after_free_error,
741
+ "owning document has been freed");
742
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
743
+ rb_raise(c_readonly_error,
744
+ "document is readonly — mutation attempted");
745
+ node = (void *)(uintptr_t)NUM2ULL(addr);
746
+ rb_ivar_set(document, id_iv_version,
747
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
748
+ st = f_elem_set_name(node, StringValueCStr(name));
749
+ check_status_c(st);
750
+ return Qtrue;
751
+ }
752
+
753
+ /* Element text or text-node content — kind-dispatched in C. */
754
+ static VALUE nf_set_binding_text(VALUE self, VALUE document,
755
+ VALUE addr, VALUE content)
756
+ {
757
+ void *node;
758
+ int st;
759
+
760
+ (void)self;
761
+ resolve_binding_classes();
762
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
763
+ rb_raise(c_use_after_free_error,
764
+ "owning document has been freed");
765
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
766
+ rb_raise(c_readonly_error,
767
+ "document is readonly — mutation attempted");
768
+ node = (void *)(uintptr_t)NUM2ULL(addr);
769
+ rb_ivar_set(document, id_iv_version,
770
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
771
+ st = f_node_type(node) == NT_ELEMENT
772
+ ? f_elem_set_text(node, StringValueCStr(content))
773
+ : f_text_set_content(node, StringValueCStr(content));
774
+ check_status_c(st);
775
+ return Qtrue;
776
+ }
777
+
778
+ static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
779
+ VALUE addr)
780
+ {
781
+ void *node;
782
+ int st;
783
+
784
+ (void)self;
785
+ resolve_binding_classes();
786
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
787
+ rb_raise(c_use_after_free_error,
788
+ "owning document has been freed");
789
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
790
+ rb_raise(c_readonly_error,
791
+ "document is readonly — mutation attempted");
792
+ node = (void *)(uintptr_t)NUM2ULL(addr);
793
+ rb_ivar_set(document, id_iv_version,
794
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
795
+ st = f_node_unlink(node);
796
+ check_status_c(st);
797
+ return Qtrue;
798
+ }
799
+
800
+ /* C-bound root= (TODO.perf/33): gates + bump + engine set_root
801
+ * in one dispatch. The caller keeps the lift decision and the
802
+ * @root memo seed (wrap gives the right document association). */
803
+ static VALUE nf_set_binding_root(VALUE self, VALUE document,
804
+ VALUE addr)
805
+ {
806
+ void *node;
807
+ int st;
808
+
809
+ (void)self;
810
+ resolve_binding_classes();
811
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
812
+ rb_raise(c_use_after_free_error,
813
+ "owning document has been freed");
814
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
815
+ rb_raise(c_readonly_error,
816
+ "document is readonly — mutation attempted");
817
+ node = (void *)(uintptr_t)NUM2ULL(addr);
818
+ rb_ivar_set(document, id_iv_version,
819
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
820
+ st = f_set_root(doc_ptr_of(document), node);
821
+ check_status_c(st);
822
+ return Qtrue;
823
+ }
824
+
825
+ /* ---- C-yield traversal (TODO.perf/27) ----------------------------
826
+ * Same contracts as the Ruby FFI::Function versions without the
827
+ * per-call closure: traverse is post-order with abort-at-self
828
+ * (the receiver is the LAST node of its subtree in post-order,
829
+ * so stopping at self bounds the walk exactly) and
830
+ * stash-abort-raise exception discipline; visit passes (node,
831
+ * entering, depth). Walk state lives on the caller's C stack —
832
+ * nested traversals are independent. */
833
+ struct trav_state {
834
+ VALUE document;
835
+ VALUE err;
836
+ void *self_ptr;
837
+ int for_visit;
838
+ };
839
+
840
+ static VALUE trav_yield_one(VALUE arg)
841
+ {
842
+ return rb_yield(arg);
843
+ }
844
+
845
+ static int trav_cb(void *node_ptr, void *user)
846
+ {
847
+ struct trav_state *st = user;
848
+ VALUE cache, key, node;
849
+ int kind, state;
850
+
851
+ if (st->err != Qnil)
852
+ return 1;
853
+ kind = f_node_type(node_ptr);
854
+ cache = binding_cache_of(st->document);
855
+ key = ULL2NUM((uint64_t)(uintptr_t)node_ptr);
856
+ node = rb_hash_aref(cache, key);
857
+ if (NIL_P(node)) {
858
+ node = rb_obj_alloc(binding_klass_for(kind));
859
+ rb_iv_set(node, "@c_address", key);
860
+ rb_iv_set(node, "@document", st->document);
861
+ rb_iv_set(node, "@parent", Qnil);
862
+ if (rb_obj_class(st->document) == c_iteration_scope) {
863
+ rb_iv_set(node, "@structure_memoizable", Qfalse);
864
+ rb_iv_set(node, "@native_fast", Qfalse);
865
+ rb_iv_set(node, "@pub_document", Qnil);
866
+ } else {
867
+ rb_iv_set(node, "@structure_memoizable", Qtrue);
868
+ rb_iv_set(node, "@native_fast", Qtrue);
869
+ rb_iv_set(node, "@pub_document", st->document);
870
+ }
871
+ rb_iv_set(node, "@addr_reads_fast", Qtrue);
872
+ rb_iv_set(node, "@node_type", INT2FIX(kind));
873
+ rb_hash_aset(cache, key, node);
874
+ }
875
+ rb_protect(trav_yield_one, node, &state);
876
+ if (state) {
877
+ st->err = rb_errinfo();
878
+ return 1; /* abort the walk */
879
+ }
880
+ return node_ptr == st->self_ptr ? 1 : 0; /* abort-at-self */
881
+ }
882
+
883
+ static VALUE nf_traverse_binding(VALUE self, VALUE document,
884
+ VALUE addr)
885
+ {
886
+ struct trav_state st;
887
+
888
+ (void)self;
889
+ resolve_binding_classes();
890
+ st.document = document;
891
+ st.err = Qnil;
892
+ st.self_ptr = (void *)(uintptr_t)NUM2ULL(addr);
893
+ st.for_visit = 0;
894
+ f_node_traverse(st.self_ptr, 1 /* TRAVERSE_POST_ORDER */,
895
+ trav_cb, &st);
896
+ if (st.err != Qnil)
897
+ rb_exc_raise(st.err);
898
+ return Qnil;
899
+ }
900
+
901
+ struct visit_yield_args {
902
+ VALUE node, entering, depth;
903
+ };
904
+
905
+ static VALUE trav_yield_args(VALUE arg)
906
+ {
907
+ struct visit_yield_args *a = (struct visit_yield_args *)arg;
908
+ return rb_yield_values(3, a->node, a->entering, a->depth);
909
+ }
910
+
911
+ /* Engine visit callback order is (user, node, entering, depth) —
912
+ * the Ruby closure's |_, node_ptr, entering, depth| mirrors it. */
913
+ static void visit_cb(void *user, void *node_ptr, int entering,
914
+ int depth)
915
+ {
916
+ /* visit's engine signature cannot abort; exceptions are
917
+ * stashed and every later node is skipped — the raise
918
+ * happens after the walk, same observable outcome. */
919
+ struct trav_state *st = user;
920
+ struct visit_yield_args va;
921
+ VALUE cache, key, node;
922
+ int kind, state;
923
+
924
+ if (st->err != Qnil)
925
+ return;
926
+ kind = f_node_type(node_ptr);
927
+ cache = binding_cache_of(st->document);
928
+ key = ULL2NUM((uint64_t)(uintptr_t)node_ptr);
929
+ node = rb_hash_aref(cache, key);
930
+ if (NIL_P(node)) {
931
+ node = rb_obj_alloc(binding_klass_for(kind));
932
+ rb_iv_set(node, "@c_address", key);
933
+ rb_iv_set(node, "@document", st->document);
934
+ rb_iv_set(node, "@parent", Qnil);
935
+ if (rb_obj_class(st->document) == c_iteration_scope) {
936
+ rb_iv_set(node, "@structure_memoizable", Qfalse);
937
+ rb_iv_set(node, "@native_fast", Qfalse);
938
+ rb_iv_set(node, "@pub_document", Qnil);
939
+ } else {
940
+ rb_iv_set(node, "@structure_memoizable", Qtrue);
941
+ rb_iv_set(node, "@native_fast", Qtrue);
942
+ rb_iv_set(node, "@pub_document", st->document);
943
+ }
944
+ rb_iv_set(node, "@addr_reads_fast", Qtrue);
945
+ rb_iv_set(node, "@node_type", INT2FIX(kind));
946
+ rb_hash_aset(cache, key, node);
947
+ }
948
+ va.node = node;
949
+ va.entering = entering ? Qtrue : Qfalse;
950
+ va.depth = INT2NUM(depth);
951
+ rb_protect(trav_yield_args, (VALUE)&va, &state);
952
+ if (state)
953
+ st->err = rb_errinfo();
954
+ }
955
+
956
+ static VALUE nf_visit_binding(VALUE self, VALUE document, VALUE addr)
957
+ {
958
+ struct trav_state st;
959
+
960
+ (void)self;
961
+ resolve_binding_classes();
962
+ st.document = document;
963
+ st.err = Qnil;
964
+ st.self_ptr = (void *)(uintptr_t)NUM2ULL(addr);
965
+ st.for_visit = 1;
966
+ f_node_visit(st.self_ptr, visit_cb, &st);
967
+ if (st.err != Qnil)
968
+ rb_exc_raise(st.err);
969
+ return Qnil;
970
+ }
971
+
972
+ /* ---- Address-based fills (TODO.perf/28) -------------------------
973
+ * Pure reads of node-local data — no cache, no version — the
974
+ * same shapes the inner_html pass uses. */
975
+ static VALUE nf_fast_text_content(VALUE self, VALUE addr)
976
+ {
977
+ const char *s;
978
+ (void)self;
979
+ s = f_text_content((void *)(uintptr_t)NUM2ULL(addr));
980
+ return s ? rb_utf8_str_new_cstr(s) : Qnil;
981
+ }
982
+
983
+ static VALUE nf_fast_comment_content(VALUE self, VALUE addr)
984
+ {
985
+ const char *s;
986
+ (void)self;
987
+ s = f_comment_content((void *)(uintptr_t)NUM2ULL(addr));
988
+ return s ? rb_utf8_str_new_cstr(s) : Qnil;
989
+ }
990
+
991
+ static VALUE nf_fast_cdata_content(VALUE self, VALUE addr)
992
+ {
993
+ const char *s;
994
+ (void)self;
995
+ s = f_cdata_content((void *)(uintptr_t)NUM2ULL(addr));
996
+ return s ? rb_utf8_str_new_cstr(s) : Qnil;
997
+ }
998
+
999
+ static VALUE nf_fast_pi_target(VALUE self, VALUE addr)
1000
+ {
1001
+ const char *s;
1002
+ (void)self;
1003
+ s = f_pi_target((void *)(uintptr_t)NUM2ULL(addr));
1004
+ return s ? rb_utf8_str_new_cstr(s) : Qnil;
1005
+ }
1006
+
1007
+ static VALUE nf_fast_pi_data(VALUE self, VALUE addr)
1008
+ {
1009
+ const char *s;
1010
+ (void)self;
1011
+ s = f_pi_data((void *)(uintptr_t)NUM2ULL(addr));
1012
+ return s ? rb_utf8_str_new_cstr(s) : Qnil;
1013
+ }
1014
+
1015
+ /* Shared construction for address-walk faces (TODO.perf/30):
1016
+ * cache lookup + kind-dispatched binding wrapper. */
1017
+ static VALUE wrap_addr_node(VALUE document, void *ptr)
1018
+ {
1019
+ VALUE cache, key, node;
1020
+ int kind;
1021
+
1022
+ kind = f_node_type(ptr);
1023
+ cache = binding_cache_of(document);
1024
+ key = ULL2NUM((uint64_t)(uintptr_t)ptr);
1025
+ node = rb_hash_aref(cache, key);
1026
+ if (NIL_P(node)) {
1027
+ node = rb_obj_alloc(binding_klass_for(kind));
1028
+ rb_iv_set(node, "@c_address", key);
1029
+ rb_iv_set(node, "@document", document);
1030
+ rb_iv_set(node, "@parent", Qnil);
1031
+ if (rb_obj_class(document) == c_iteration_scope) {
1032
+ rb_iv_set(node, "@structure_memoizable", Qfalse);
1033
+ rb_iv_set(node, "@native_fast", Qfalse);
1034
+ rb_iv_set(node, "@pub_document", Qnil);
1035
+ } else {
1036
+ rb_iv_set(node, "@structure_memoizable", Qtrue);
1037
+ rb_iv_set(node, "@native_fast", Qtrue);
1038
+ rb_iv_set(node, "@pub_document", document);
1039
+ }
1040
+ rb_iv_set(node, "@addr_reads_fast", Qtrue);
1041
+ rb_iv_set(node, "@node_type", INT2FIX(kind));
1042
+ rb_hash_aset(cache, key, node);
1043
+ }
1044
+ return node;
1045
+ }
1046
+
1047
+ /* First element child in one C walk (text-heavy parents paid two
1048
+ * FFI calls per skipped sibling). */
1049
+ static VALUE nf_first_element_child(VALUE self, VALUE document,
1050
+ VALUE addr)
1051
+ {
1052
+ void *p;
1053
+
1054
+ (void)self;
1055
+ resolve_binding_classes();
1056
+ for (p = f_first_child((void *)(uintptr_t)NUM2ULL(addr));
1057
+ p != NULL; p = f_next_sibling(p)) {
1058
+ if (f_node_type(p) == NT_ELEMENT)
1059
+ return wrap_addr_node(document, p);
1060
+ }
1061
+ return Qnil;
1062
+ }
1063
+
1064
+ /* Last element child in one C walk (the batch fetch materialized
1065
+ * every child to keep one). */
1066
+ static VALUE nf_last_element_child(VALUE self, VALUE document,
1067
+ VALUE addr)
1068
+ {
1069
+ void *p, *last = NULL;
1070
+
1071
+ (void)self;
1072
+ resolve_binding_classes();
1073
+ for (p = f_first_child((void *)(uintptr_t)NUM2ULL(addr));
1074
+ p != NULL; p = f_next_sibling(p)) {
1075
+ if (f_node_type(p) == NT_ELEMENT)
1076
+ last = p;
1077
+ }
1078
+ return last ? wrap_addr_node(document, last) : Qnil;
1079
+ }
1080
+
1081
+ /* Owned string: copy + free through the engine's seam (the
1082
+ * read_owned_string protocol). */
1083
+ static VALUE nf_fast_path(VALUE self, VALUE addr)
1084
+ {
1085
+ const char *s;
1086
+ VALUE out;
1087
+ (void)self;
1088
+ s = f_node_xpath((void *)(uintptr_t)NUM2ULL(addr));
1089
+ if (!s)
1090
+ return Qnil;
1091
+ out = rb_utf8_str_new_cstr(s);
1092
+ f_free_str((void *)s);
1093
+ return out;
1094
+ }
1095
+
1096
+ /* C-bound insert family (TODO.perf/14): prepend / insert-after /
1097
+ * insert-before share append's gates + predicate + bump shape.
1098
+ * mode: 1 prepend, 2 after, 3 before (anchor = receiver). */
1099
+ static VALUE nf_insert_binding_child(VALUE self, VALUE document,
1100
+ VALUE anchor_addr, VALUE child_addr,
1101
+ VALUE mode)
1102
+ {
1103
+ void *anchor, *child;
1104
+ const char *uri;
1105
+ int st, m;
1106
+
1107
+ (void)self;
1108
+ resolve_binding_classes();
1109
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
1110
+ rb_raise(c_use_after_free_error,
1111
+ "owning document has been freed");
1112
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
1113
+ rb_raise(c_readonly_error,
1114
+ "document is readonly — mutation attempted");
1115
+ anchor = (void *)(uintptr_t)NUM2ULL(anchor_addr);
1116
+ child = (void *)(uintptr_t)NUM2ULL(child_addr);
1117
+ m = FIX2INT(mode);
1118
+ if (f_node_type(child) == NT_ELEMENT) {
1119
+ if (f_elem_ns_count(child) > 0)
1120
+ return Qnil;
1121
+ uri = f_elem_ns(child);
1122
+ if (uri && *uri)
1123
+ return Qnil;
1124
+ }
1125
+ rb_ivar_set(document, id_iv_version,
1126
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
1127
+ switch (m) {
1128
+ case 1: st = f_prepend_child(anchor, child); break;
1129
+ case 2: st = f_insert_after(anchor, child); break;
1130
+ case 3: st = f_insert_before(anchor, child); break;
1131
+ default:
1132
+ rb_raise(rb_eArgError, "invalid insert mode %d", m);
1133
+ return Qnil;
1134
+ }
1135
+ check_status_c(st);
1136
+ return Qtrue;
1137
+ }
1138
+
1139
+ /* Builder factories (#149): create in C, wrap as NativeNode — no
1140
+ * FFI::Pointer, no Ruby wrap_fresh path. */
1141
+ static VALUE nn_create_element(VALUE klass, VALUE document, VALUE name)
1142
+ {
1143
+ void *ptr;
1144
+ (void)klass;
1145
+ ptr = f_elem_create(doc_ptr_of(document), StringValueCStr(name));
1146
+ if (!ptr)
1147
+ rb_raise(rb_eRuntimeError, "leptris_element_create failed");
1148
+ return wrap_new(document, ptr);
1149
+ }
1150
+
1151
+ static VALUE nn_create_text(VALUE klass, VALUE document, VALUE content)
1152
+ {
1153
+ void *ptr;
1154
+ (void)klass;
1155
+ ptr = f_text_create(doc_ptr_of(document), StringValueCStr(content));
1156
+ if (!ptr)
1157
+ rb_raise(rb_eRuntimeError, "leptris_text_node_create failed");
1158
+ return wrap_new(document, ptr);
1159
+ }
1160
+
1161
+ static VALUE nn_create_child(VALUE self, VALUE name)
1162
+ {
1163
+ struct native_node *n;
1164
+ void *ptr;
1165
+ VALUE out;
1166
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
1167
+ document_advance_version(n->document);
1168
+ ptr = f_create_child(n->ptr, StringValueCStr(name));
1169
+ if (!ptr)
1170
+ rb_raise(rb_eRuntimeError, "leptris_element_create_child failed");
1171
+ out = wrap_new(n->document, ptr);
1172
+ return out;
1173
+ }
1174
+
1175
+ static VALUE nn_append_child(VALUE self, VALUE child)
1176
+ {
1177
+ struct native_node *n, *c;
1178
+ int rc;
1179
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
1180
+ TypedData_Get_Struct(child, struct native_node, &nn_type, c);
1181
+ document_advance_version(n->document);
1182
+ rc = f_append_child(n->ptr, c->ptr);
1183
+ if (rc != 0)
1184
+ rb_raise(rb_eRuntimeError, "leptris_element_append_child failed (%d)", rc);
1185
+ return child;
1186
+ }
1187
+
1188
+ static VALUE nn_set_root(VALUE klass, VALUE document, VALUE element)
1189
+ {
1190
+ struct native_node *n;
1191
+ int rc;
1192
+ (void)klass;
1193
+ TypedData_Get_Struct(element, struct native_node, &nn_type, n);
1194
+ document_advance_version(document);
1195
+ rc = f_set_root(doc_ptr_of(document), n->ptr);
1196
+ if (rc != 0)
1197
+ rb_raise(rb_eRuntimeError, "leptris_document_set_root failed (%d)", rc);
1198
+ return element;
1199
+ }
1200
+
1201
+ /* NativeNode surface completion (TODO.perf/05): bulk attribute
1202
+ * hash (name => value, one C walk, no per-attr Ruby frames) and
1203
+ * the position readers. */
1204
+ static VALUE nn_attributes(VALUE self)
1205
+ {
1206
+ struct native_node *n;
1207
+ VALUE hash;
1208
+ void *attr;
1209
+
1210
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
1211
+ hash = rb_hash_new();
1212
+ attr = f_attr_first(n->ptr);
1213
+ while (attr) {
1214
+ const char *name = f_attr_name(attr);
1215
+ const char *value = f_attr_value(n->ptr, attr);
1216
+ rb_hash_aset(hash,
1217
+ name ? rb_utf8_str_new_cstr(name) : Qnil,
1218
+ value ? rb_utf8_str_new_cstr(value) : Qnil);
1219
+ attr = f_attr_next(attr);
1220
+ }
1221
+ return hash;
1222
+ }
1223
+
1224
+ static VALUE nn_line(VALUE self)
1225
+ {
1226
+ struct native_node *n;
1227
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
1228
+ return UINT2NUM(f_node_line(n->ptr));
1229
+ }
1230
+
1231
+ static VALUE nn_byte_offset(VALUE self)
1232
+ {
1233
+ struct native_node *n;
1234
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
1235
+ return ULL2NUM((uint64_t)f_node_offset(n->ptr));
1236
+ }
1237
+
1238
+ /* The owning binding Document (moxml #213: adapters route
1239
+ * native.document exactly like Node#document). */
1240
+ static VALUE nn_document(VALUE self)
1241
+ {
1242
+ struct native_node *n;
1243
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
1244
+ return n->document;
1245
+ }
1246
+
1247
+ static VALUE nn_address(VALUE self)
1248
+ {
1249
+ struct native_node *n;
1250
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
1251
+ return ULL2NUM((uint64_t)(uintptr_t)n->ptr);
1252
+ }
1253
+
1254
+ /* ---- Bulk children for BINDING wrappers (TODO.perf/01 tail) ----
1255
+ * One C pass: children_ex + binding-class dispatch + ivar set +
1256
+ * identity-cache check/store. Returns a Ruby Array of Element/
1257
+ * Text/... wrappers — Node#children swaps this in when the ext
1258
+ * is loaded. Class constants and FFI::Pointer resolve once at
1259
+ * Init; the per-node Ruby frames (wrap/construct/memo/alive)
1260
+ * disappear. */
1261
+ static VALUE c_b_element = Qundef, c_b_text, c_b_comment, c_b_cdata,
1262
+ c_b_pi, c_b_entity_ref, c_b_node, c_b_result_text,
1263
+ c_b_result_attr,
1264
+ c_b_attr, c_ffi_pointer, c_b_document, c_b_freed;
1265
+ static VALUE c_iteration_scope;
1266
+ static ID id_ptr_new;
1267
+
1268
+ /* Binding classes resolve LAZILY: Init_native can run before the
1269
+ * binding's autoload entries load (require "leptris" flow) —
1270
+ * rb_path2class at Init time raises for undefined constants. */
1271
+ static void resolve_binding_classes(void)
1272
+ {
1273
+ if (c_b_element != Qundef)
1274
+ return;
1275
+ c_b_element = rb_path2class("Leptris::XML::Element");
1276
+ c_b_text = rb_path2class("Leptris::XML::Text");
1277
+ c_b_comment = rb_path2class("Leptris::XML::Comment");
1278
+ c_b_cdata = rb_path2class("Leptris::XML::CDATA");
1279
+ c_b_pi = rb_path2class("Leptris::XML::ProcessingInstruction");
1280
+ c_b_entity_ref = rb_path2class("Leptris::XML::EntityReference");
1281
+ c_b_node = rb_path2class("Leptris::XML::Node");
1282
+ c_b_result_text = rb_path2class("Leptris::XML::ResultText");
1283
+ c_b_result_attr = rb_path2class("Leptris::XML::ResultAttr");
1284
+ c_ffi_pointer = rb_path2class("FFI::Pointer");
1285
+ c_b_attr = rb_path2class("Leptris::XML::Attr");
1286
+ c_b_document = rb_path2class("Leptris::XML::Document");
1287
+ c_b_freed = rb_path2class("Leptris::XML::Document::Freed");
1288
+ c_iteration_scope = rb_path2class("Leptris::XML::IterationScope");
1289
+ c_leptris_error = rb_path2class("Leptris::XML::Error");
1290
+ id_ptr_new = rb_intern("new");
1291
+ rb_gc_register_mark_object(c_b_element);
1292
+ rb_gc_register_mark_object(c_b_text);
1293
+ rb_gc_register_mark_object(c_b_comment);
1294
+ rb_gc_register_mark_object(c_b_cdata);
1295
+ rb_gc_register_mark_object(c_b_pi);
1296
+ rb_gc_register_mark_object(c_b_entity_ref);
1297
+ rb_gc_register_mark_object(c_b_node);
1298
+ rb_gc_register_mark_object(c_b_result_text);
1299
+ rb_gc_register_mark_object(c_b_result_attr);
1300
+ rb_gc_register_mark_object(c_ffi_pointer);
1301
+ rb_gc_register_mark_object(c_b_attr);
1302
+ rb_gc_register_mark_object(c_b_document);
1303
+ rb_gc_register_mark_object(c_b_freed);
1304
+ rb_gc_register_mark_object(c_iteration_scope);
1305
+ rb_gc_register_mark_object(c_leptris_error);
1306
+ }
1307
+
1308
+ static VALUE binding_klass_for(int kind)
1309
+ {
1310
+ switch (kind) {
1311
+ case 0: return c_b_element;
1312
+ case 1: return c_b_text;
1313
+ case 2: return c_b_comment;
1314
+ case 3: return c_b_cdata;
1315
+ case 4: return c_b_pi;
1316
+ case 10: return c_b_entity_ref; /* #212 / upstream #1094 */
1317
+ default: return c_b_node;
1318
+ }
1319
+ }
1320
+
1321
+ static VALUE bulk_children_impl(VALUE document, VALUE parent_addr,
1322
+ int elements_only)
1323
+ {
1324
+ void **buf;
1325
+ int *kinds;
1326
+ int count, i;
1327
+ VALUE cache, out;
1328
+
1329
+ count = fetch_children_two_call((void *)(uintptr_t)NUM2ULL(parent_addr),
1330
+ &buf, &kinds);
1331
+ if (count <= 0) {
1332
+ ruby_xfree(buf);
1333
+ ruby_xfree(kinds);
1334
+ return rb_ary_new2(0);
1335
+ }
1336
+
1337
+ cache = binding_cache_of(document);
1338
+ out = rb_ary_new2(elements_only ? 8 : count);
1339
+ for (i = 0; i < count; i++) {
1340
+ VALUE key, node;
1341
+ if (elements_only && kinds[i] != 0)
1342
+ continue;
1343
+ key = ULL2NUM((uint64_t)(uintptr_t)buf[i]);
1344
+ node = rb_hash_aref(cache, key);
1345
+ if (NIL_P(node)) {
1346
+ node = rb_obj_alloc(binding_klass_for(kinds[i]));
1347
+ rb_iv_set(node, "@c_address", key);
1348
+ rb_iv_set(node, "@document", document);
1349
+ rb_iv_set(node, "@parent", Qnil);
1350
+ /* TODO.perf/25: scope-aware stamps — the IterationScope
1351
+ * flows through here as "document" for iterparse
1352
+ * elements; those stay memo-exempt and FFI-mutating,
1353
+ * but address-based reads are cache-free and safe. */
1354
+ if (rb_obj_class(document) == c_iteration_scope) {
1355
+ rb_iv_set(node, "@structure_memoizable", Qfalse);
1356
+ rb_iv_set(node, "@native_fast", Qfalse);
1357
+ rb_iv_set(node, "@pub_document", Qnil);
1358
+ } else {
1359
+ rb_iv_set(node, "@structure_memoizable", Qtrue);
1360
+ rb_iv_set(node, "@native_fast", Qtrue);
1361
+ rb_iv_set(node, "@pub_document", document);
1362
+ }
1363
+ rb_iv_set(node, "@addr_reads_fast", Qtrue);
1364
+ rb_iv_set(node, "@node_type", INT2FIX(kinds[i]));
1365
+ rb_hash_aset(cache, key, node);
1366
+ }
1367
+ rb_ary_push(out, node);
1368
+ }
1369
+ ruby_xfree(buf);
1370
+ ruby_xfree(kinds);
1371
+ return out;
1372
+ }
1373
+
1374
+ static VALUE nf_bulk_children(VALUE self, VALUE document, VALUE parent_addr)
1375
+ {
1376
+ (void)self;
1377
+ resolve_binding_classes();
1378
+ return bulk_children_impl(document, parent_addr, 0);
1379
+ }
1380
+
1381
+ static VALUE nf_bulk_element_children(VALUE self, VALUE document,
1382
+ VALUE parent_addr)
1383
+ {
1384
+ (void)self;
1385
+ resolve_binding_classes();
1386
+ return bulk_children_impl(document, parent_addr, 1);
1387
+ }
1388
+
1389
+ /* ---- Bulk XPath result materialization (TODO.perf/03) ----------
1390
+ * One C pass over leptris_xpath_result_get_nodes_ex: binding-class
1391
+ * dispatch (XPATH_NODE space: 0 element, 1 attribute, 2 text,
1392
+ * 3 other), value capture for synthetic text/attribute items
1393
+ * while the result handle is alive, identity-cache check/store
1394
+ * for elements. Returns the Ruby Array. */
1395
+ static VALUE m_native_sentinel; /* module object = fallback marker */
1396
+ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
1397
+ void *result, void *ptr, int kind,
1398
+ int idx);
1399
+ static VALUE binding_klass_for(int kind);
1400
+ static VALUE c_iteration_scope;
1401
+
1402
+ static VALUE bulk_xpath_array(VALUE document, void *result);
1403
+
1404
+ static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
1405
+ {
1406
+ (void)self;
1407
+ return bulk_xpath_array(document,
1408
+ (void *)(uintptr_t)NUM2ULL(result_ptr_val));
1409
+ }
1410
+
1411
+ /* TODO.perf/22: materialize AND free — the eager xpath path owns
1412
+ * the handle lifecycle in C; no Ruby AutoPointer, no finalizer. */
1413
+ static VALUE nf_materialize_xpath(VALUE self, VALUE document,
1414
+ VALUE result_ptr_val)
1415
+ {
1416
+ void *result = (void *)(uintptr_t)NUM2ULL(result_ptr_val);
1417
+ VALUE out;
1418
+ long i, n;
1419
+
1420
+ (void)self;
1421
+ out = bulk_xpath_array(document, result);
1422
+ n = RARRAY_LEN(out);
1423
+ for (i = 0; i < n; i++) {
1424
+ if (rb_ary_entry(out, i) == m_native_sentinel) {
1425
+ /* Exotic kinds need the Ruby fallback per entry — the
1426
+ * caller keeps the handle and goes the lazy path. */
1427
+ return Qnil;
1428
+ }
1429
+ }
1430
+ f_xp_free(result);
1431
+ return out;
1432
+ }
1433
+
1434
+ static VALUE bulk_xpath_array(VALUE document, void *result)
1435
+ {
1436
+ void **buf;
1437
+ int *kinds;
1438
+ int count, i;
1439
+ VALUE cache, out;
1440
+
1441
+ resolve_binding_classes();
1442
+ size_t scount = f_xp_count(result);
1443
+ if (scount == 0)
1444
+ return rb_ary_new2(0);
1445
+
1446
+ buf = ruby_xmalloc(scount * sizeof(void *));
1447
+ kinds = ruby_xmalloc(scount * sizeof(int));
1448
+ scount = f_xp_nodes_ex(result, buf, kinds, scount);
1449
+ count = (int)scount;
1450
+ if (count <= 0) {
1451
+ ruby_xfree(buf);
1452
+ ruby_xfree(kinds);
1453
+ return rb_ary_new2(0);
1454
+ }
1455
+ cache = binding_cache_of(document);
1456
+ out = rb_ary_new2(count);
1457
+ for (i = 0; i < count; i++) {
1458
+ if (buf[i] == NULL)
1459
+ continue;
1460
+ rb_ary_push(out, materialize_xp_entry(document, cache, result,
1461
+ buf[i], kinds[i], i));
1462
+ }
1463
+ ruby_xfree(buf);
1464
+ ruby_xfree(kinds);
1465
+ return out;
1466
+ }
1467
+
1468
+ /* Materializes ONE result entry as a binding wrapper (or the
1469
+ * module sentinel for the rare kinds the bulk path defers to
1470
+ * Ruby). Shared by the bulk materializer (TODO.perf/03) and the
1471
+ * at_xpath single-result seam (TODO.perf/16). */
1472
+ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
1473
+ void *result, void *ptr, int kind,
1474
+ int idx)
1475
+ {
1476
+ VALUE node, key;
1477
+ const char *s;
1478
+
1479
+ key = ULL2NUM((uint64_t)(uintptr_t)ptr);
1480
+ switch (kind) {
1481
+ case 0: /* element */
1482
+ node = rb_hash_aref(cache, key);
1483
+ if (NIL_P(node)) {
1484
+ node = rb_obj_alloc(c_b_element);
1485
+ rb_iv_set(node, "@c_address", key);
1486
+ rb_iv_set(node, "@document", document);
1487
+ rb_iv_set(node, "@parent", Qnil);
1488
+ rb_iv_set(node, "@structure_memoizable", Qtrue);
1489
+ rb_iv_set(node, "@native_fast", Qtrue);
1490
+ rb_iv_set(node, "@pub_document", document);
1491
+ rb_iv_set(node, "@node_type", INT2FIX(0));
1492
+ rb_hash_aset(cache, key, node);
1493
+ }
1494
+ return node;
1495
+ case 1: /* synthetic attribute */
1496
+ {
1497
+ const char *name = f_xp_node_name(result, idx);
1498
+ const char *value = f_xp_node_value(result, idx);
1499
+ node = rb_obj_alloc(c_b_result_attr);
1500
+ rb_iv_set(node, "@c_address", key);
1501
+ rb_iv_set(node, "@document", document);
1502
+ rb_iv_set(node, "@attr_name",
1503
+ name ? rb_utf8_str_new_cstr(name) : Qnil);
1504
+ rb_iv_set(node, "@attr_value",
1505
+ value ? rb_utf8_str_new_cstr(value) : Qnil);
1506
+ }
1507
+ return node;
1508
+ case 2: /* text-kind: synthetic sequence carrier, or a real
1509
+ * Text/CDATA node (XPath reports both as TEXT) —
1510
+ * dispatch on the node's own type like Node.wrap. */
1511
+ {
1512
+ int nt = f_node_type(ptr);
1513
+ if (nt == 8) { /* NODE_SYNTHETIC_TEXT */
1514
+ s = f_xp_node_value(result, idx);
1515
+ node = rb_obj_alloc(c_b_result_text);
1516
+ rb_iv_set(node, "@c_address", key);
1517
+ rb_iv_set(node, "@document", document);
1518
+ rb_iv_set(node, "@value",
1519
+ s ? rb_utf8_str_new_cstr(s) : Qnil);
1520
+ return node;
1521
+ }
1522
+ node = rb_hash_aref(cache, key);
1523
+ if (NIL_P(node)) {
1524
+ node = rb_obj_alloc(binding_klass_for(nt));
1525
+ rb_iv_set(node, "@c_address", key);
1526
+ rb_iv_set(node, "@document", document);
1527
+ rb_iv_set(node, "@parent", Qnil);
1528
+ rb_iv_set(node, "@structure_memoizable", Qtrue);
1529
+ rb_iv_set(node, "@native_fast", Qtrue);
1530
+ rb_iv_set(node, "@pub_document", document);
1531
+ rb_iv_set(node, "@node_type", INT2FIX(nt));
1532
+ rb_hash_aset(cache, key, node);
1533
+ }
1534
+ }
1535
+ return node;
1536
+ default: /* other: rare — signal Ruby fallback for this entry */
1537
+ return m_native_sentinel;
1538
+ }
1539
+ }
1540
+
1541
+ /* ---- at_xpath single-result seam (TODO.perf/16) ----------------
1542
+ * One dispatch for the hottest query shape: type check, entry-0
1543
+ * materialization through the bulk machinery, result free.
1544
+ * Non-nodeset results return Qundef — the caller keeps the exact
1545
+ * Ruby scalar path (and owns the free there). */
1546
+ static VALUE nf_at_xpath_first(VALUE self, VALUE document,
1547
+ VALUE result_ptr_val)
1548
+ {
1549
+ void *result = (void *)(uintptr_t)NUM2ULL(result_ptr_val);
1550
+ void *buf;
1551
+ int kind;
1552
+ size_t scount;
1553
+ VALUE node;
1554
+
1555
+ (void)self;
1556
+ resolve_binding_classes();
1557
+ if (f_xp_type(result) != 0) /* XPATH_NODESET */
1558
+ return m_native_sentinel; /* caller keeps the scalar path */
1559
+ scount = f_xp_count(result);
1560
+ if (scount == 0) {
1561
+ f_xp_free(result);
1562
+ return Qnil;
1563
+ }
1564
+ buf = NULL;
1565
+ scount = f_xp_nodes_ex(result, &buf, &kind, 1);
1566
+ if (scount == 0 || buf == NULL) {
1567
+ f_xp_free(result);
1568
+ return Qnil;
1569
+ }
1570
+ node = materialize_xp_entry(document, binding_cache_of(document),
1571
+ result, buf, kind, 0);
1572
+ f_xp_free(result);
1573
+ return node;
1574
+ }
1575
+
1576
+ /* ---- Bulk attribute materialization (TODO.perf/10) ---------------
1577
+ * Element#attributes / #keys / #values walk the attribute list
1578
+ * one FFI call per row (name + value). One C pass builds the
1579
+ * {name => value} hash for the binding. */
1580
+ static VALUE nf_bulk_attributes(VALUE self, VALUE addr)
1581
+ {
1582
+ void *node = (void *)(uintptr_t)NUM2ULL(addr);
1583
+ VALUE hash = rb_hash_new();
1584
+ void *attr = f_attr_first(node);
1585
+
1586
+ (void)self;
1587
+ while (attr) {
1588
+ const char *name = f_attr_name(attr);
1589
+ const char *value = f_attr_value(node, attr);
1590
+ rb_hash_aset(hash,
1591
+ name ? rb_utf8_str_new_cstr(name) : Qnil,
1592
+ value ? rb_utf8_str_new_cstr(value) : Qnil);
1593
+ attr = f_attr_next(attr);
1594
+ }
1595
+ return hash;
1596
+ }
1597
+
1598
+ /* ---- Binding create via C (TODO.perf/09): one call creates the
1599
+ * node AND constructs the binding wrapper (class dispatch, ivars,
1600
+ * identity-cache store) — no FFI marshaling, no wrap_fresh path. */
1601
+ static VALUE nf_create_binding_element(VALUE self, VALUE document,
1602
+ VALUE name)
1603
+ {
1604
+ void *doc = doc_ptr_of(document);
1605
+ void *ptr = f_elem_create(doc, RSTRING_PTR(StringValue(name)));
1606
+ VALUE node, cache, key;
1607
+
1608
+ (void)self;
1609
+ resolve_binding_classes();
1610
+ if (!ptr)
1611
+ return Qnil; /* caller raises with the error channel */
1612
+ node = rb_obj_alloc(c_b_element);
1613
+ rb_iv_set(node, "@c_address",
1614
+ ULL2NUM((uint64_t)(uintptr_t)ptr));
1615
+ rb_iv_set(node, "@document", document);
1616
+ rb_iv_set(node, "@parent", Qnil);
1617
+ rb_iv_set(node, "@structure_memoizable", Qtrue);
1618
+ rb_iv_set(node, "@native_fast", Qtrue);
1619
+ rb_iv_set(node, "@pub_document", document);
1620
+ rb_iv_set(node, "@node_type", INT2FIX(0));
1621
+ cache = binding_cache_of(document);
1622
+ key = ULL2NUM((uint64_t)(uintptr_t)ptr);
1623
+ rb_hash_aset(cache, key, node);
1624
+ return node;
1625
+ }
1626
+
1627
+ static VALUE nf_create_binding_text(VALUE self, VALUE document,
1628
+ VALUE content)
1629
+ {
1630
+ void *doc = doc_ptr_of(document);
1631
+ void *ptr = f_text_create(doc, RSTRING_PTR(StringValue(content)));
1632
+ VALUE node, cache, key;
1633
+
1634
+ (void)self;
1635
+ resolve_binding_classes();
1636
+ if (!ptr)
1637
+ return Qnil;
1638
+ node = rb_obj_alloc(c_b_text);
1639
+ rb_iv_set(node, "@c_address",
1640
+ ULL2NUM((uint64_t)(uintptr_t)ptr));
1641
+ rb_iv_set(node, "@document", document);
1642
+ rb_iv_set(node, "@parent", Qnil);
1643
+ rb_iv_set(node, "@structure_memoizable", Qtrue);
1644
+ rb_iv_set(node, "@native_fast", Qtrue);
1645
+ rb_iv_set(node, "@pub_document", document);
1646
+ rb_iv_set(node, "@node_type", INT2FIX(1));
1647
+ cache = binding_cache_of(document);
1648
+ key = ULL2NUM((uint64_t)(uintptr_t)ptr);
1649
+ rb_hash_aset(cache, key, node);
1650
+ return node;
1651
+ }
1652
+
1653
+ /* ---- Bulk attribute FACES (TODO.perf/10): {name => Attr} with
1654
+ * the values hash beside it — the exact shapes Element#attributes
1655
+ * memoizes, built in one walk. Attr is a value object
1656
+ * (name/value/element ivars; c_handle optional). */
1657
+
1658
+ static ID id_iv_name, id_iv_value, id_iv_element;
1659
+
1660
+ static VALUE nf_bulk_attr_faces(VALUE self, VALUE addr, VALUE element)
1661
+ {
1662
+ void *node = (void *)(uintptr_t)NUM2ULL(addr);
1663
+ VALUE faces = rb_hash_new();
1664
+ VALUE values = rb_hash_new();
1665
+ void *attr = f_attr_first(node);
1666
+
1667
+ (void)self;
1668
+ resolve_binding_classes();
1669
+ while (attr) {
1670
+ const char *name = f_attr_name(attr);
1671
+ const char *value = f_attr_value(node, attr);
1672
+ VALUE k = name ? rb_utf8_str_new_cstr(name) : Qnil;
1673
+ VALUE v = value ? rb_utf8_str_new_cstr(value) : Qnil;
1674
+ VALUE a = rb_obj_alloc(c_b_attr);
1675
+ rb_iv_set(a, "@name", k);
1676
+ rb_iv_set(a, "@value", v);
1677
+ rb_iv_set(a, "@element", element);
1678
+ rb_hash_aset(faces, k, a);
1679
+ rb_hash_aset(values, k, v);
1680
+ attr = f_attr_next(attr);
1681
+ }
1682
+ return rb_assoc_new(faces, values);
1683
+ }
1684
+
1685
+ /* ---- Ext-bound serialization (TODO.perf/04) ---------------------
1686
+ * The whole sized-buffer cycle in C: build SerializeOptions on
1687
+ * the stack, call (buf, cap, NULL status, opts), grow if needed,
1688
+ * return a UTF-8 String. No FFI marshaling, no Ruby buffer
1689
+ * management per to_xml call. */
1690
+ struct serialize_opts {
1691
+ int indent;
1692
+ int xml_declaration;
1693
+ const char *encoding;
1694
+ };
1695
+
1696
+ /* Grow-only scratch for large serializations. Safe under the GVL:
1697
+ * synchronous C calls hold it, so no two Ruby threads run this
1698
+ * concurrently. */
1699
+ static char *ser_buf;
1700
+ static size_t ser_cap;
1701
+
1702
+ static VALUE fast_serialize(serialize_into_fn fn, void *node,
1703
+ int indent, int xml_declaration,
1704
+ VALUE encoding)
1705
+ {
1706
+ struct serialize_opts opts;
1707
+ char stack_buf[4096];
1708
+ size_t needed;
1709
+
1710
+ char *probe;
1711
+ size_t probe_cap;
1712
+
1713
+ opts.indent = indent;
1714
+ opts.xml_declaration = xml_declaration;
1715
+ /* The Ruby string's bytes stay valid across the synchronous
1716
+ * engine call (TODO.perf/36). */
1717
+ opts.encoding = NIL_P(encoding)
1718
+ ? NULL
1719
+ : StringValueCStr(encoding);
1720
+ /* Probe the largest buffer we own: a big scratch from a prior
1721
+ * call usually fits (single serialization); cold calls start
1722
+ * on the stack buffer. */
1723
+ if (ser_cap > sizeof(stack_buf)) {
1724
+ probe = ser_buf;
1725
+ probe_cap = ser_cap;
1726
+ } else {
1727
+ probe = stack_buf;
1728
+ probe_cap = sizeof(stack_buf);
1729
+ }
1730
+ needed = fn(node, probe, probe_cap, NULL, &opts);
1731
+ if (needed == 0)
1732
+ return rb_utf8_str_new_cstr("");
1733
+ if (needed <= probe_cap)
1734
+ return rb_utf8_str_new(probe, needed - 1);
1735
+ if (needed > ser_cap) {
1736
+ ser_buf = ruby_xrealloc(ser_buf, needed);
1737
+ ser_cap = needed;
1738
+ }
1739
+ needed = fn(node, ser_buf, ser_cap, NULL, &opts);
1740
+ return needed > 0 ? rb_utf8_str_new(ser_buf, needed - 1)
1741
+ : rb_utf8_str_new_cstr("");
1742
+ }
1743
+
1744
+ static VALUE nf_fast_document_xml(VALUE self, VALUE addr,
1745
+ VALUE indent, VALUE decl,
1746
+ VALUE encoding)
1747
+ {
1748
+ (void)self;
1749
+ return fast_serialize(f_doc_serialize,
1750
+ (void *)(uintptr_t)NUM2ULL(addr),
1751
+ NUM2INT(indent), RTEST(decl) ? 1 : 0,
1752
+ encoding);
1753
+ }
1754
+
1755
+ static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
1756
+ VALUE indent, VALUE decl,
1757
+ VALUE encoding)
1758
+ {
1759
+ (void)self;
1760
+ return fast_serialize(f_elem_serialize,
1761
+ (void *)(uintptr_t)NUM2ULL(addr),
1762
+ NUM2INT(indent), RTEST(decl) ? 1 : 0,
1763
+ encoding);
1764
+ }
1765
+
1766
+ /* ---- Bulk hydration walker (TODO.perf/38, #230) -----------------
1767
+ * Pre-order walk that materializes one Hash per element node
1768
+ * (kind, name, prefix, uri, attrs as Array<[name,value]>, text,
1769
+ * depth, line) and yields it to a Ruby Proc (walk_subtree) or
1770
+ * collects it into an Array (snapshot). No per-node Node wrapper
1771
+ * construction — the consumer iterates rows and builds their
1772
+ * typed objects directly. */
1773
+ /* Node kinds (mirror types.h). Documented in descriptor comment. */
1774
+ #define WS_NODE_ELEMENT 0
1775
+ #define WS_NODE_TEXT 1
1776
+ #define WS_NODE_COMMENT 2
1777
+ #define WS_NODE_CDATA 3
1778
+ #define WS_NODE_PI 4
1779
+ #define WS_NODE_DOCTYPE 5
1780
+
1781
+ static VALUE build_element_row(VALUE doc, void *elem, int depth)
1782
+ {
1783
+ VALUE row = rb_hash_new();
1784
+ rb_hash_aset(row, ID2SYM(rb_intern("kind")),
1785
+ rb_utf8_str_new_cstr("element"));
1786
+ const char *name = f_elem_name(elem);
1787
+ rb_hash_aset(row, ID2SYM(rb_intern("name")),
1788
+ name ? rb_utf8_str_new_cstr(name) : Qnil);
1789
+ rb_hash_aset(row, ID2SYM(rb_intern("prefix")),
1790
+ f_elem_prefix(elem) ? rb_utf8_str_new_cstr(f_elem_prefix(elem)) : Qnil);
1791
+ rb_hash_aset(row, ID2SYM(rb_intern("uri")),
1792
+ f_elem_ns(elem) ? rb_utf8_str_new_cstr(f_elem_ns(elem)) : Qnil);
1793
+ /* attrs: name/value pairs as Array<[name, value]>. */
1794
+ VALUE attrs = rb_ary_new();
1795
+ void *a = f_attr_first(elem);
1796
+ while (a) {
1797
+ VALUE pair = rb_ary_new_capa(2);
1798
+ const char *an = f_attr_name(a);
1799
+ const char *av = f_attr_value(elem, a);
1800
+ rb_ary_push(pair, an ? rb_utf8_str_new_cstr(an) : Qnil);
1801
+ rb_ary_push(pair, av ? rb_utf8_str_new_cstr(av) : Qnil);
1802
+ rb_ary_push(attrs, pair);
1803
+ a = f_attr_next(a);
1804
+ }
1805
+ rb_hash_aset(row, ID2SYM(rb_intern("attrs")), attrs);
1806
+ /* text: first text-child content (moxml-style common case). */
1807
+ void *c = f_first_child(elem);
1808
+ VALUE text = Qnil;
1809
+ while (c) {
1810
+ if (f_node_type(c) == WS_NODE_TEXT) {
1811
+ const char *t = f_text_content(c);
1812
+ text = t ? rb_utf8_str_new_cstr(t) : Qnil;
1813
+ break;
1814
+ }
1815
+ c = f_next_sibling(c);
1816
+ }
1817
+ rb_hash_aset(row, ID2SYM(rb_intern("text")), text);
1818
+ rb_hash_aset(row, ID2SYM(rb_intern("depth")), INT2NUM(depth));
1819
+ return row;
1820
+ }
1821
+
1822
+ static VALUE build_text_row(int kind, int depth, const char *t)
1823
+ {
1824
+ VALUE row = rb_hash_new();
1825
+ rb_hash_aset(row, ID2SYM(rb_intern("kind")),
1826
+ rb_utf8_str_new_cstr(kind == WS_NODE_TEXT ? "text"
1827
+ : kind == WS_NODE_COMMENT ? "comment"
1828
+ : kind == WS_NODE_CDATA ? "cdata" : "other"));
1829
+ rb_hash_aset(row, ID2SYM(rb_intern("text")),
1830
+ t ? rb_utf8_str_new_cstr(t) : Qnil);
1831
+ rb_hash_aset(row, ID2SYM(rb_intern("depth")), INT2NUM(depth));
1832
+ return row;
1833
+ }
1834
+
1835
+ static void yield_or_collect_row(VALUE out_or_block, VALUE row, int collect)
1836
+ {
1837
+ if (collect) {
1838
+ rb_ary_push(out_or_block, row);
1839
+ } else {
1840
+ rb_funcall(out_or_block, rb_intern("call"), 1, row);
1841
+ }
1842
+ }
1843
+
1844
+ static void walk_subtree_impl(VALUE doc, void *node, int depth, VALUE out_or_block, int collect)
1845
+ {
1846
+ int kind = f_node_type(node);
1847
+ if (kind == WS_NODE_ELEMENT) {
1848
+ void *c;
1849
+ yield_or_collect_row(out_or_block, build_element_row(doc, node, depth), collect);
1850
+ c = f_first_child(node);
1851
+ while (c) {
1852
+ walk_subtree_impl(doc, c, depth + 1, out_or_block, collect);
1853
+ c = f_next_sibling(c);
1854
+ }
1855
+ } else if (kind == WS_NODE_DOCTYPE || kind == 9 /* DOCUMENT */) {
1856
+ void *dc = f_first_child(node);
1857
+ while (dc) {
1858
+ walk_subtree_impl(doc, dc, depth, out_or_block, collect);
1859
+ dc = f_next_sibling(dc);
1860
+ }
1861
+ } else if (kind == WS_NODE_TEXT || kind == WS_NODE_COMMENT ||
1862
+ kind == WS_NODE_CDATA || kind == WS_NODE_PI) {
1863
+ const char *t = NULL;
1864
+ if (kind == WS_NODE_TEXT)
1865
+ t = f_text_content(node);
1866
+ else if (kind == WS_NODE_COMMENT)
1867
+ t = f_comment_content(node);
1868
+ else if (kind == WS_NODE_CDATA)
1869
+ t = f_cdata_content(node);
1870
+ else if (kind == WS_NODE_PI)
1871
+ t = f_pi_data(node);
1872
+ yield_or_collect_row(out_or_block, build_text_row(kind, depth, t), collect);
1873
+ }
1874
+ }
1875
+
1876
+ static VALUE nf_snapshot_subtree(VALUE self, VALUE document, VALUE addr)
1877
+ {
1878
+ VALUE out = rb_ary_new();
1879
+ walk_subtree_impl(document, (void *)(uintptr_t)NUM2ULL(addr), 0, out, 1);
1880
+ return out;
1881
+ }
1882
+
1883
+ /* ---- Lean plan rows (lutaml/moxml#249 plan layer) ----------------
1884
+ * Elements only, one 4-tuple per element: [name, attrs_flat, text,
1885
+ * depth] — no Hash per row, no text/comment rows, attr and element
1886
+ * NAMES interned (shared frozen fstrings: names repeat across the
1887
+ * document, so repeat reads allocate zero). Pre-order, depth 0 at
1888
+ * the addressed node. */
1889
+ static void walk_rows_impl(void *node, int depth, VALUE out)
1890
+ {
1891
+ if (f_node_type(node) == WS_NODE_ELEMENT) {
1892
+ VALUE row = rb_ary_new_capa(4);
1893
+ VALUE attrs;
1894
+ const char *name = f_elem_name(node);
1895
+ const char *an;
1896
+ const char *av;
1897
+ void *a;
1898
+ void *c;
1899
+
1900
+ rb_ary_store(row, 0, name ? rb_enc_interned_str(name, strlen(name),
1901
+ rb_utf8_encoding())
1902
+ : Qnil);
1903
+ attrs = rb_ary_new();
1904
+ for (a = f_attr_first(node); a; a = f_attr_next(a)) {
1905
+ an = f_attr_name(a);
1906
+ av = f_attr_value(node, a);
1907
+ rb_ary_push(attrs, an ? rb_enc_interned_str(an, strlen(an),
1908
+ rb_utf8_encoding())
1909
+ : Qnil);
1910
+ rb_ary_push(attrs, av ? rb_utf8_str_new_cstr(av) : Qnil);
1911
+ }
1912
+ rb_ary_store(row, 1, attrs);
1913
+ for (c = f_first_child(node); c; c = f_next_sibling(c)) {
1914
+ if (f_node_type(c) == WS_NODE_TEXT) {
1915
+ const char *t = f_text_content(c);
1916
+ rb_ary_store(row, 2, t ? rb_utf8_str_new_cstr(t) : Qnil);
1917
+ break;
1918
+ }
1919
+ }
1920
+ rb_ary_store(row, 3, INT2NUM(depth));
1921
+ rb_ary_push(out, row);
1922
+
1923
+ for (c = f_first_child(node); c; c = f_next_sibling(c)) {
1924
+ walk_rows_impl(c, depth + 1, out);
1925
+ }
1926
+ } else if (f_node_type(node) == WS_NODE_DOCTYPE || f_node_type(node) == 9) {
1927
+ void *dc = f_first_child(node);
1928
+ while (dc) {
1929
+ walk_rows_impl(dc, depth, out);
1930
+ dc = f_next_sibling(dc);
1931
+ }
1932
+ }
1933
+ }
1934
+
1935
+ static VALUE nf_snapshot_rows(VALUE self, VALUE document, VALUE addr)
1936
+ {
1937
+ (void)self;
1938
+ (void)document;
1939
+ VALUE out = rb_ary_new();
1940
+ walk_rows_impl((void *)(uintptr_t)NUM2ULL(addr), 0, out);
1941
+ return out;
1942
+ }
1943
+
1944
+ /* ---- inner_html in one C pass (TODO.perf/18) --------------------
1945
+ * Serializes the receiver's children into one growable buffer:
1946
+ * elements via leptris_element_serialize_into (the same opts the
1947
+ * Ruby loop passed), text XML-escaped with the binding's entity
1948
+ * set (& < > \r), CDATA/comments/PIs wrapped raw — byte-identical
1949
+ * to Element#inner_html's per-child loop. */
1950
+ static char *inner_buf;
1951
+ static size_t inner_cap;
1952
+
1953
+ static void inner_ensure(size_t need)
1954
+ {
1955
+ if (need > inner_cap) {
1956
+ inner_cap = need < 64 ? 64 : need;
1957
+ inner_buf = ruby_xrealloc(inner_buf, inner_cap);
1958
+ }
1959
+ }
1960
+
1961
+ static size_t inner_append_raw(const char *s, size_t len, size_t at)
1962
+ {
1963
+ inner_ensure(at + len + 1);
1964
+ if (len)
1965
+ memcpy(inner_buf + at, s, len);
1966
+ return at + len;
1967
+ }
1968
+
1969
+ static size_t inner_append_escaped(const char *s, size_t at)
1970
+ {
1971
+ for (; s && *s; s++) {
1972
+ const char *repl;
1973
+ size_t rl;
1974
+ switch (*s) {
1975
+ case '&': repl = "&amp;"; rl = 5; break;
1976
+ case '<': repl = "&lt;"; rl = 4; break;
1977
+ case '>': repl = "&gt;"; rl = 4; break;
1978
+ case '\r': repl = "&#xD;"; rl = 5; break;
1979
+ default: repl = NULL; rl = 0; break;
1980
+ }
1981
+ if (repl) {
1982
+ at = inner_append_raw(repl, rl, at);
1983
+ } else {
1984
+ inner_ensure(at + 2);
1985
+ inner_buf[at++] = *s;
1986
+ }
1987
+ }
1988
+ return at;
1989
+ }
1990
+
1991
+ static VALUE nf_fast_inner_xml(VALUE self, VALUE addr)
1992
+ {
1993
+ void *node = (void *)(uintptr_t)NUM2ULL(addr);
1994
+ void *child;
1995
+ struct serialize_opts opts;
1996
+ size_t at = 0, needed;
1997
+
1998
+ (void)self;
1999
+ opts.indent = 0;
2000
+ opts.xml_declaration = 1; /* matches element_xml_default */
2001
+ opts.encoding = NULL;
2002
+ inner_ensure(4096);
2003
+ for (child = f_first_child(node); child != NULL;
2004
+ child = f_next_sibling(child)) {
2005
+ switch (f_node_type(child)) {
2006
+ case 0: /* element */
2007
+ needed = f_elem_serialize(child, inner_buf + at,
2008
+ inner_cap - at, NULL, &opts);
2009
+ if (needed > 0) {
2010
+ if (needed > inner_cap - at) {
2011
+ inner_ensure(at + needed);
2012
+ needed = f_elem_serialize(child, inner_buf + at,
2013
+ inner_cap - at, NULL, &opts);
2014
+ }
2015
+ at += needed - 1; /* size includes the NUL */
2016
+ }
2017
+ break;
2018
+ case 1: /* text */
2019
+ at = inner_append_escaped(f_text_content(child), at);
2020
+ break;
2021
+ case 2: /* comment */
2022
+ at = inner_append_raw("<!--", 4, at);
2023
+ at = inner_append_escaped(f_comment_content(child), at);
2024
+ at = inner_append_raw("-->", 3, at);
2025
+ break;
2026
+ case 3: { /* CDATA */
2027
+ const char *c = f_cdata_content(child);
2028
+ at = inner_append_raw("<![CDATA[", 9, at);
2029
+ at = inner_append_raw(c, c ? strlen(c) : 0, at);
2030
+ at = inner_append_raw("]]>", 3, at);
2031
+ break;
2032
+ }
2033
+ case 4: { /* PI — the binding strips leading whitespace from
2034
+ * the data before joining (read_pi_data parity). */
2035
+ const char *data = f_pi_data(child);
2036
+ while (data && (*data == ' ' || *data == '\t' ||
2037
+ *data == '\r' || *data == '\n'))
2038
+ data++;
2039
+ at = inner_append_raw("<?", 2, at);
2040
+ at = inner_append_escaped(f_pi_target(child), at);
2041
+ if (data && *data) {
2042
+ at = inner_append_raw(" ", 1, at);
2043
+ at = inner_append_escaped(data, at);
2044
+ }
2045
+ at = inner_append_raw("?>", 2, at);
2046
+ break;
2047
+ }
2048
+ default:
2049
+ break;
2050
+ }
2051
+ }
2052
+ return rb_utf8_str_new(inner_buf, at);
2053
+ }
2054
+
2055
+ /* ---- Adoption-lift predicate (TODO.perf/09, #204 ask 3) ---------
2056
+ * The #178/#208 namespace lift + pruning runs on EVERY attach and
2057
+ * costs several FFI round-trips even when provably a no-op. This
2058
+ * predicate answers in ONE dispatch: false when the node carries
2059
+ * no namespace declarations AND its name has no prefix — the
2060
+ * common programmatic-build shape (bare names, no namespaces). */
2061
+ /* ---- Document lifetime in C (TODO.perf/12) ----------------------
2062
+ * A TypedData handle holding the C document pointer, referenced
2063
+ * only by the binding Document's @doc_handle ivar — its lifetime
2064
+ * IS the document's. dfree releases the C document directly: no
2065
+ * Ruby finalizer, no FFI dispatch from finalizer context.
2066
+ * Document#free releases the pointer first, so dfree no-ops —
2067
+ * the same double-free protocol the Freed struct enforces on the
2068
+ * Ruby-finalizer path (which stays for LEPTRIS_NO_NATIVE). */
2069
+ struct doc_handle {
2070
+ void *doc;
2071
+ };
2072
+
2073
+ static void dh_free(void *p)
2074
+ {
2075
+ struct doc_handle *h = p;
2076
+ if (h->doc) {
2077
+ f_doc_free(h->doc);
2078
+ h->doc = NULL;
2079
+ }
2080
+ }
2081
+
2082
+ static size_t dh_size(const void *p)
2083
+ {
2084
+ (void)p;
2085
+ return sizeof(struct doc_handle);
2086
+ }
2087
+
2088
+ static const rb_data_type_t dh_type = {
2089
+ "Leptris/XML/DocHandle",
2090
+ { 0, dh_free, dh_size, },
2091
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
2092
+ };
2093
+
2094
+ static VALUE c_doc_handle;
2095
+ static ID id_iv_doc_handle, id_freed_new, id_alive;
2096
+
2097
+ /* Node#dup in one dispatch (TODO.perf/30): engine create +
2098
+ * lifetime handle + document wrapper + element_copy + the copy
2099
+ * wrapped-and-rooted (memo-seeded). The namespace lift stays a
2100
+ * Ruby decision (skip_adoption_lift? — the copy_of seam's
2101
+ * semantics, #696/#721/#812). */
2102
+ static VALUE nf_copy_binding_element(VALUE self, VALUE document,
2103
+ VALUE addr)
2104
+ {
2105
+ void *doc, *copy;
2106
+ VALUE new_doc, doc_addr, handle, freed, root;
2107
+ struct doc_handle *h;
2108
+
2109
+ (void)self;
2110
+ resolve_binding_classes();
2111
+ doc = f_doc_create();
2112
+ if (!doc)
2113
+ return Qnil;
2114
+ doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
2115
+ new_doc = rb_obj_alloc(c_b_document);
2116
+ rb_iv_set(new_doc, "@c_address", doc_addr);
2117
+ freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
2118
+ rb_iv_set(new_doc, "@freed", freed);
2119
+ rb_iv_set(new_doc, "@readonly", Qfalse);
2120
+ rb_iv_set(new_doc, "@version", INT2FIX(0));
2121
+ handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
2122
+ &dh_type, h);
2123
+ h->doc = doc;
2124
+ rb_ivar_set(new_doc, id_iv_doc_handle, handle);
2125
+
2126
+ copy = f_elem_copy((void *)(uintptr_t)NUM2ULL(addr), doc);
2127
+ if (!copy) {
2128
+ h->doc = NULL; /* the handle releases the empty doc */
2129
+ return Qnil;
2130
+ }
2131
+ root = wrap_addr_node(new_doc, copy);
2132
+ rb_ivar_set(new_doc, rb_intern("@root"), root);
2133
+ rb_ivar_set(new_doc, rb_intern("@root_version"), INT2FIX(0));
2134
+ return new_doc;
2135
+ }
2136
+
2137
+ /* Shared document-wrapper construction from a C document
2138
+ * pointer: ivar-seeded wrapper + lifetime handle. Returns the
2139
+ * wrapper. (TODO.perf/35 — the create face inlines the same
2140
+ * steps.) */
2141
+ static VALUE build_binding_document(void *doc)
2142
+ {
2143
+ VALUE new_doc, doc_addr, handle, freed;
2144
+ struct doc_handle *h;
2145
+
2146
+ doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
2147
+ new_doc = rb_obj_alloc(c_b_document);
2148
+ rb_iv_set(new_doc, "@c_ptr",
2149
+ rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
2150
+ rb_iv_set(new_doc, "@c_address", doc_addr);
2151
+ freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
2152
+ rb_iv_set(new_doc, "@freed", freed);
2153
+ rb_iv_set(new_doc, "@readonly", Qfalse);
2154
+ rb_iv_set(new_doc, "@version", INT2FIX(0));
2155
+ handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
2156
+ &dh_type, h);
2157
+ h->doc = doc;
2158
+ rb_ivar_set(new_doc, id_iv_doc_handle, handle);
2159
+ return new_doc;
2160
+ }
2161
+
2162
+ /* check_status in C (TODO.perf/36): the exact status_message
2163
+ * format — "base" or "base (detail)" — so the mutation faces
2164
+ * raise instead of returning codes for a Ruby dispatch. */
2165
+ static void check_status_c(int st)
2166
+ {
2167
+ const char *base, *detail;
2168
+
2169
+ if (st == 0)
2170
+ return;
2171
+ base = f_status_str(st);
2172
+ detail = f_last_err();
2173
+ if (detail && *detail)
2174
+ rb_raise(c_leptris_error, "%s (%s)",
2175
+ base ? base : "?", detail);
2176
+ rb_raise(c_leptris_error, "%s", base ? base : "?");
2177
+ }
2178
+
2179
+ /* Document.parse default path in one dispatch (TODO.perf/35):
2180
+ * leptris_parse_string + wrapper + handle. Qnil = parse failure
2181
+ * (the caller raises with the same message shape). */
2182
+ static VALUE nf_parse_binding_document(VALUE self, VALUE xml)
2183
+ {
2184
+ void *doc;
2185
+
2186
+ (void)self;
2187
+ resolve_binding_classes();
2188
+ doc = f_parse_str(RSTRING_PTR(StringValue(xml)),
2189
+ (size_t)RSTRING_LEN(StringValue(xml)), NULL);
2190
+ return doc ? build_binding_document(doc) : Qnil;
2191
+ }
2192
+
2193
+ /* Fragment lane (TODO.perf/34): parse the markup against the
2194
+ * document; returns the fragment's ADDRESS, or Qfalse when the
2195
+ * parse fails (status is not surfaced — the caller re-runs the
2196
+ * FFI path for the exact error). */
2197
+ static VALUE nf_parse_fragment_addr(VALUE self, VALUE document,
2198
+ VALUE xml)
2199
+ {
2200
+ void *frag;
2201
+ int status;
2202
+
2203
+ (void)self;
2204
+ resolve_binding_classes();
2205
+ frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
2206
+ (size_t)RSTRING_LEN(StringValue(xml)),
2207
+ doc_ptr_of(document), &status);
2208
+ return frag ? ULL2NUM((uint64_t)(uintptr_t)frag) : Qfalse;
2209
+ }
2210
+
2211
+ /* One-shot markup append (TODO.perf/34): parse the markup, walk
2212
+ * the fragment's children, append each to the parent — one
2213
+ * readonly gate + version bump for the whole batch. Returns the
2214
+ * appended count; -1 = parse failure; status codes surface as
2215
+ * negatives (-1000 - st) for the caller's check_status path. */
2216
+ static VALUE nf_append_markup(VALUE self, VALUE document,
2217
+ VALUE parent_addr, VALUE xml)
2218
+ {
2219
+ void *frag, *parent, *child, *next_child;
2220
+ int status, count = 0;
2221
+
2222
+ (void)self;
2223
+ resolve_binding_classes();
2224
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
2225
+ rb_raise(c_use_after_free_error,
2226
+ "owning document has been freed");
2227
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
2228
+ rb_raise(c_readonly_error,
2229
+ "document is readonly — mutation attempted");
2230
+ frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
2231
+ (size_t)RSTRING_LEN(StringValue(xml)),
2232
+ doc_ptr_of(document), &status);
2233
+ if (!frag)
2234
+ return INT2FIX(-1);
2235
+ parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
2236
+ rb_ivar_set(document, id_iv_version,
2237
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
2238
+ /* Appending DETACHES the child from the fragment — capture
2239
+ * the next sibling before each move or the walk corrupts. */
2240
+ for (child = f_first_child(frag); child != NULL; child = next_child) {
2241
+ next_child = f_next_sibling(child);
2242
+ status = f_append_child(parent, child);
2243
+ if (status != 0)
2244
+ check_status_c(status);
2245
+ count++;
2246
+ }
2247
+ return INT2FIX(count);
2248
+ }
2249
+
2250
+ static VALUE nf_doc_handle_attach(VALUE self, VALUE document)
2251
+ {
2252
+ struct doc_handle *h;
2253
+ VALUE handle;
2254
+
2255
+ (void)self;
2256
+ handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
2257
+ &dh_type, h);
2258
+ h->doc = doc_ptr_of(document);
2259
+ rb_ivar_set(document, id_iv_doc_handle, handle);
2260
+ return handle;
2261
+ }
2262
+
2263
+ /* Document#free already released the C memory through the FFI
2264
+ * seam; detach so the GC-pass dfree no-ops. */
2265
+ static VALUE nf_doc_handle_release(VALUE self, VALUE document)
2266
+ {
2267
+ VALUE handle = rb_ivar_get(document, id_iv_doc_handle);
2268
+ (void)self;
2269
+ if (handle != Qnil) {
2270
+ struct doc_handle *h;
2271
+ TypedData_Get_Struct(handle, struct doc_handle, &dh_type, h);
2272
+ h->doc = NULL;
2273
+ }
2274
+ return Qnil;
2275
+ }
2276
+
2277
+ /* The full Document.create in one dispatch: engine create,
2278
+ * binding wrapper (ivar-seeded, bypassing initialize), and the
2279
+ * lifetime handle. Returns Qnil when the engine refuses. */
2280
+ static VALUE nf_create_binding_document(VALUE self)
2281
+ {
2282
+ void *doc;
2283
+ VALUE document, addr, handle, freed;
2284
+ struct doc_handle *h;
2285
+
2286
+ (void)self;
2287
+ resolve_binding_classes();
2288
+ doc = f_doc_create();
2289
+ if (!doc)
2290
+ return Qnil;
2291
+ addr = ULL2NUM((uint64_t)(uintptr_t)doc);
2292
+ document = rb_obj_alloc(c_b_document);
2293
+ rb_iv_set(document, "@c_ptr",
2294
+ rb_funcall(c_ffi_pointer, id_ptr_new, 1, addr));
2295
+ rb_iv_set(document, "@c_address", addr);
2296
+ freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
2297
+ rb_iv_set(document, "@freed", freed);
2298
+ rb_iv_set(document, "@readonly", Qfalse);
2299
+ rb_iv_set(document, "@version", INT2FIX(0));
2300
+ handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
2301
+ &dh_type, h);
2302
+ h->doc = doc;
2303
+ rb_ivar_set(document, id_iv_doc_handle, handle);
2304
+ return document;
2305
+ }
2306
+
2307
+ /* ---- Address-based fast readers (TODO.perf/01) -----------------
2308
+ * The DEFAULT binding classes call these when the bundle is
2309
+ * loaded: one C-API dispatch + rb_utf8_str_new_cstr — no FFI
2310
+ * marshaling. Addresses come from FFI::Pointer#address (a Ruby
2311
+ * ivar read on the ffi gem's Pointer). */
2312
+ static VALUE nf_name(VALUE self, VALUE addr)
2313
+ {
2314
+ const char *s;
2315
+ (void)self;
2316
+ s = f_elem_name((void *)(uintptr_t)NUM2ULL(addr));
2317
+ return s ? rb_utf8_str_new_cstr(s) : Qnil;
2318
+ }
2319
+
2320
+ static VALUE nf_element_text(VALUE self, VALUE addr)
2321
+ {
2322
+ const char *s;
2323
+ (void)self;
2324
+ s = f_element_text((void *)(uintptr_t)NUM2ULL(addr));
2325
+ return s ? rb_utf8_str_new_cstr(s) : Qnil;
2326
+ }
2327
+
2328
+ static VALUE nf_attribute(VALUE self, VALUE addr, VALUE name)
2329
+ {
2330
+ const char *s;
2331
+ (void)self;
2332
+ s = f_attr((void *)(uintptr_t)NUM2ULL(addr), StringValueCStr(name));
2333
+ return s ? rb_utf8_str_new_cstr(s) : Qnil;
2334
+ }
2335
+
2336
+ static VALUE nf_prefix(VALUE self, VALUE addr)
2337
+ {
2338
+ const char *s;
2339
+ (void)self;
2340
+ s = f_elem_prefix((void *)(uintptr_t)NUM2ULL(addr));
2341
+ return (s && *s) ? rb_utf8_str_new_cstr(s) : Qnil;
2342
+ }
2343
+
2344
+ static VALUE nn_from(VALUE klass, VALUE document, VALUE element)
2345
+ {
2346
+ /* Element address comes through the binding's #c_ptr address;
2347
+ * registered in the shared identity cache so parent/sibling
2348
+ * round-trips return the same object. */
2349
+ struct native_node *n;
2350
+ VALUE node = nn_allocate(klass);
2351
+ void *ptr = (void *)(uintptr_t)NUM2ULL(
2352
+ rb_funcall(element, id_address, 0));
2353
+ TypedData_Get_Struct(node, struct native_node, &nn_type, n);
2354
+ n->ptr = ptr;
2355
+ n->document = document;
2356
+ rb_hash_aset(native_cache_of(document),
2357
+ ULL2NUM((uint64_t)(uintptr_t)ptr), node);
2358
+ return node;
2359
+ }
2360
+
2361
+ /* Ruby-side bootstrap: resolve the libleptris symbols once, from
2362
+ * the path the FFI layer loaded. */
2363
+ static VALUE leptris_native_resolve_rb(VALUE self, VALUE lib_path)
2364
+ {
2365
+ (void)self;
2366
+ resolve_symbols(StringValueCStr(lib_path));
2367
+ return Qnil;
2368
+ }
2369
+
2370
+ void Init_native(void)
2371
+ {
2372
+ VALUE m_leptris, m_xml, m_native;
2373
+
2374
+ m_leptris = rb_define_module("Leptris");
2375
+ m_xml = rb_define_module_under(m_leptris, "XML");
2376
+ c_native_node = rb_define_class_under(m_xml, "NativeNode", rb_cObject);
2377
+ rb_undef_alloc_func(c_native_node);
2378
+
2379
+ id_iv_name = rb_intern("@name");
2380
+ id_iv_value = rb_intern("@value");
2381
+ id_iv_element = rb_intern("@element");
2382
+ id_iv_c_address = rb_intern("@c_address");
2383
+ id_iv_native_cache = rb_intern("@native_cache");
2384
+ id_iv_binding_cache = rb_intern("@wrapper_cache");
2385
+ id_iv_version = rb_intern("@version");
2386
+ id_iv_readonly = rb_intern("@readonly");
2387
+ id_address = rb_intern("address");
2388
+ id_iv_nn_content = rb_intern("@nn_content");
2389
+ id_iv_nn_content_ver = rb_intern("@nn_content_ver");
2390
+ id_iv_nn_attrs = rb_intern("@nn_attrs");
2391
+ id_iv_nn_attrs_ver = rb_intern("@nn_attrs_ver");
2392
+ id_child_klasses = rb_intern("@nn_child_klasses");
2393
+ id_iv_doc_handle = rb_intern("@doc_handle");
2394
+ id_freed_new = rb_intern("new");
2395
+ id_alive = rb_intern("alive");
2396
+ c_readonly_error = rb_path2class("Leptris::XML::ReadOnlyError");
2397
+ c_use_after_free_error =
2398
+ rb_path2class("Leptris::XML::UseAfterFreeError");
2399
+ rb_gc_register_mark_object(c_readonly_error);
2400
+ rb_gc_register_mark_object(c_use_after_free_error);
2401
+
2402
+ rb_define_singleton_method(c_native_node, "from", nn_from, 2);
2403
+ rb_define_singleton_method(c_native_node, "install_child_klasses",
2404
+ nn_install_child_klasses, 1);
2405
+ rb_define_singleton_method(c_native_node, "create_element", nn_create_element, 2);
2406
+ rb_define_singleton_method(c_native_node, "create_text", nn_create_text, 2);
2407
+ rb_define_singleton_method(c_native_node, "set_root", nn_set_root, 2);
2408
+ rb_define_method(c_native_node, "create_child", nn_create_child, 1);
2409
+ rb_define_method(c_native_node, "append_child", nn_append_child, 1);
2410
+ rb_define_method(c_native_node, "add_child", nn_append_child, 1);
2411
+ rb_define_method(c_native_node, "address", nn_address, 0);
2412
+ rb_define_method(c_native_node, "document", nn_document, 0);
2413
+ rb_define_method(c_native_node, "attributes", nn_attributes, 0);
2414
+ rb_define_method(c_native_node, "line", nn_line, 0);
2415
+ rb_define_method(c_native_node, "byte_offset", nn_byte_offset, 0);
2416
+ rb_define_method(c_native_node, "name", nn_name, 0);
2417
+ rb_define_method(c_native_node, "content", nn_content, 0);
2418
+ rb_define_method(c_native_node, "attribute", nn_attribute, 1);
2419
+ rb_define_alias(c_native_node, "[]", "attribute");
2420
+ /* Unshadowed aliases for consumers whose contract modules shadow
2421
+ * attribute/content: a plain call resolves to these at normal
2422
+ * method-cache cost instead of UnboundMethod#bind_call. */
2423
+ rb_define_alias(c_native_node, "attr_read", "attribute");
2424
+ rb_define_alias(c_native_node, "text_read", "content");
2425
+ rb_define_method(c_native_node, "children", nn_children, 0);
2426
+ rb_define_method(c_native_node, "element_children", nn_element_children, 0);
2427
+ rb_define_method(c_native_node, "next_sibling", nn_next_sibling, 0);
2428
+ rb_define_method(c_native_node, "parent", nn_parent, 0);
2429
+ rb_define_method(c_native_node, "node_type", nn_type_sym, 0);
2430
+ rb_define_singleton_method(c_native_node, "resolve!",
2431
+ leptris_native_resolve_rb, 1);
2432
+
2433
+ m_native = rb_define_module_under(m_xml, "Native");
2434
+ m_native_sentinel = m_native;
2435
+ rb_define_module_function(m_native, "fast_name", nf_name, 1);
2436
+ rb_define_module_function(m_native, "fast_element_text", nf_element_text, 1);
2437
+ rb_define_module_function(m_native, "fast_attribute", nf_attribute, 2);
2438
+ rb_define_module_function(m_native, "fast_prefix", nf_prefix, 1);
2439
+ rb_define_module_function(m_native, "fast_attribute2", nf_fast_attribute2, 2);
2440
+ rb_define_module_function(m_native, "ns_lift_needed?", nf_ns_lift_needed, 1);
2441
+ rb_define_module_function(m_native, "append_binding_child",
2442
+ nf_append_binding_child, 3);
2443
+ rb_define_module_function(m_native, "set_binding_attribute",
2444
+ nf_set_binding_attribute, 4);
2445
+ rb_define_module_function(m_native, "insert_binding_child",
2446
+ nf_insert_binding_child, 4);
2447
+ rb_define_module_function(m_native, "at_xpath_first",
2448
+ nf_at_xpath_first, 2);
2449
+ rb_define_module_function(m_native, "materialize_xpath",
2450
+ nf_materialize_xpath, 2);
2451
+ rb_define_module_function(m_native, "set_binding_name",
2452
+ nf_set_binding_name, 3);
2453
+ rb_define_module_function(m_native, "set_binding_text",
2454
+ nf_set_binding_text, 3);
2455
+ rb_define_module_function(m_native, "unlink_binding_node",
2456
+ nf_unlink_binding_node, 2);
2457
+ rb_define_module_function(m_native, "traverse_binding",
2458
+ nf_traverse_binding, 2);
2459
+ rb_define_module_function(m_native, "visit_binding",
2460
+ nf_visit_binding, 2);
2461
+ rb_define_module_function(m_native, "fast_text_content",
2462
+ nf_fast_text_content, 1);
2463
+ rb_define_module_function(m_native, "fast_comment_content",
2464
+ nf_fast_comment_content, 1);
2465
+ rb_define_module_function(m_native, "fast_cdata_content",
2466
+ nf_fast_cdata_content, 1);
2467
+ rb_define_module_function(m_native, "fast_pi_target",
2468
+ nf_fast_pi_target, 1);
2469
+ rb_define_module_function(m_native, "fast_pi_data",
2470
+ nf_fast_pi_data, 1);
2471
+ rb_define_module_function(m_native, "fast_path",
2472
+ nf_fast_path, 1);
2473
+ rb_define_module_function(m_native, "first_element_child",
2474
+ nf_first_element_child, 2);
2475
+ rb_define_module_function(m_native, "last_element_child",
2476
+ nf_last_element_child, 2);
2477
+ rb_define_module_function(m_native, "copy_binding_element",
2478
+ nf_copy_binding_element, 2);
2479
+ rb_define_module_function(m_native, "set_binding_root",
2480
+ nf_set_binding_root, 2);
2481
+ rb_define_module_function(m_native, "parse_binding_document",
2482
+ nf_parse_binding_document, 1);
2483
+ rb_define_module_function(m_native, "parse_fragment_addr",
2484
+ nf_parse_fragment_addr, 2);
2485
+ rb_define_module_function(m_native, "append_markup",
2486
+ nf_append_markup, 3);
2487
+ rb_define_module_function(m_native, "fast_inner_xml",
2488
+ nf_fast_inner_xml, 1);
2489
+ rb_define_module_function(m_native, "snapshot_subtree",
2490
+ nf_snapshot_subtree, 2);
2491
+ rb_define_module_function(m_native, "snapshot_rows",
2492
+ nf_snapshot_rows, 2);
2493
+ rb_define_module_function(m_native, "doc_handle_attach",
2494
+ nf_doc_handle_attach, 1);
2495
+ rb_define_module_function(m_native, "doc_handle_release",
2496
+ nf_doc_handle_release, 1);
2497
+ rb_define_module_function(m_native, "create_binding_document",
2498
+ nf_create_binding_document, 0);
2499
+ c_doc_handle = rb_define_class_under(m_xml, "DocHandle", rb_cObject);
2500
+ rb_undef_alloc_func(c_doc_handle);
2501
+ rb_define_module_function(m_native, "bulk_attributes", nf_bulk_attributes, 1);
2502
+ rb_define_module_function(m_native, "bulk_attr_faces",
2503
+ nf_bulk_attr_faces, 2);
2504
+ rb_define_module_function(m_native, "create_binding_element",
2505
+ nf_create_binding_element, 2);
2506
+ rb_define_module_function(m_native, "create_binding_text",
2507
+ nf_create_binding_text, 2);
2508
+ rb_define_module_function(m_native, "bulk_children", nf_bulk_children, 2);
2509
+ rb_define_module_function(m_native, "bulk_element_children",
2510
+ nf_bulk_element_children, 2);
2511
+ rb_define_module_function(m_native, "bulk_xpath", nf_bulk_xpath, 2);
2512
+ rb_define_module_function(m_native, "fast_document_xml",
2513
+ nf_fast_document_xml, 4);
2514
+ rb_define_module_function(m_native, "fast_element_xml",
2515
+ nf_fast_element_xml, 4);
2516
+ }
2517
+
2518
+ #ifdef _WIN32
2519
+ /* MRI derives a C extension's init symbol from the require
2520
+ * feature's basename cut at the first dot. The Windows per-minor
2521
+ * DLLs are therefore named dot-free (native_3_3.so etc.), and
2522
+ * each needs its exact init name exported. All are defined here
2523
+ * unconditionally: a DLL only ever loads under the Ruby minor it
2524
+ * was built against (PE binds its build Ruby's runtime DLL). */
2525
+ void Init_native_3_3(void) { Init_native(); }
2526
+ void Init_native_3_4(void) { Init_native(); }
2527
+ void Init_native_4_0(void) { Init_native(); }
2528
+ #endif