leptris 1.9.162.1 → 1.9.162.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a0d781b64373e54b12695b3729ee006b1fd142cc72b2d46db4a6f7314e0c0e8
4
- data.tar.gz: 23acc9e2a5d5c9a014e0d480729d7f569f662274956c56fcc22e54795f050b57
3
+ metadata.gz: d09354998670281f8e6519fa0cf7b35c467d67e7536879b31be2b3efa634250e
4
+ data.tar.gz: 362d92cf0e463160f94ecab7639a60d867680e47610bed4cac408dc0e835c1fb
5
5
  SHA512:
6
- metadata.gz: 559c4771c00c196028387b1f9376581227792f3f979bbb11960807b2ce3f2f51f231fd67d1131034362eb5bd80a8ad062d5c1da59b5ad45805a68c4e2f1561da
7
- data.tar.gz: 5cb77399413c18c75ea0256a3ac6b62c98d2a4988c72b952337d0f047eee0fd4820e52961a4a4e16a1bbf09a188878e953429f24816ad7c431193e67b4bdfdde
6
+ metadata.gz: 59ee84ed455a5621ab3be63c4fb54ddf3d9b42346cb32f8f9a7c967f884bb70a04cb4e3864528718b26dfe725540f05f27ec06f64c44a252f203b40ff9484d8f
7
+ data.tar.gz: 66d52689d9be07a40fee5739959878556f393ec0c86fb94b5e36c95ab36c38d022be579ff7189561f96f91eac972035e77e645eff0b8dae272b345dbc2f9667d
data/CHANGELOG.md CHANGED
@@ -5,7 +5,72 @@ All notable changes to Leptris will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.9.162.1] - 2026-09-14
8
+ ## [1.9.162.6] - 2026-09-14
9
+
10
+ ### Fixed
11
+
12
+ - **Native layer linkage, resolved**: setup-ruby's custom rubies
13
+ link libruby by absolute runner path through mkmf no matter
14
+ which RbConfig entries are cleared (and a post-link
15
+ install_name_tool rewrite rebinds every Ruby symbol to the
16
+ replacement — 1.9.162.5's "Symbol not found: rb_cObject"). The
17
+ darwin bundle is now linked directly in the Rakefile: compile
18
+ via the Makefile, link with pure -undefined dynamic_lookup and
19
+ -mmacosx-version-min=11.0 — no libruby LC_LOAD_DYLIB can exist.
20
+ Build log prints otool -L as the standing contract check.
21
+ \n## [1.9.162.5] - 2026-09-14
22
+
23
+ ### Fixed
24
+
25
+ - **Native layer linkage, third round**: the runner-side
26
+ libruby reference came from setup-ruby's LIBRUBYARG_SHARED
27
+ (an absolute runner path recorded as LC_LOAD_DYLIB —
28
+ unresolvable on user machines), not from the link flags. The
29
+ extconf now clears the libruby link args for the bundle target
30
+ (Ruby symbols resolve from the loading interpreter); the
31
+ Rakefile prints otool -L into the build log as the standing
32
+ linkage contract check.
33
+ \n## [1.9.162.4] - 2026-09-14
34
+
35
+ ### Fixed
36
+
37
+ - **Native layer linkage, second round**: 1.9.162.3's
38
+ -undefined dynamic_lookup rode $LDFLAGS, which the release
39
+ runner's Ruby 3.3 mkmf drops from the bundle link line. The
40
+ extconf now pins BOTH $LDFLAGS and $DLDFLAGS (plus
41
+ -mmacosx-version-min in the compile flags) — verified locally:
42
+ minos 11.0, libSystem-only linkage.
43
+ \n## [1.9.162.3] - 2026-09-14
44
+
45
+ ### Fixed
46
+
47
+ - **Native layer bundle linkage**: the 1.9.162.2 platform gems'
48
+ native.bundle linked the BUILDING runner's libruby by absolute
49
+ path and targeted the runner's macOS — failing to load on user
50
+ machines. The ext now links with -undefined dynamic_lookup
51
+ (Ruby symbols resolve from the loading interpreter, the standard
52
+ C-extension convention) and a conservative deployment target;
53
+ the bundle references only libSystem.
54
+ \n## [1.9.162.2] - 2026-09-14
55
+
56
+ ### Added
57
+
58
+ - **Opt-in native read layer** (#185 / TODO.restructure/21 round
59
+ 2): `require "leptris/xml/native_layer"` activates
60
+ `Document#native_node` — TypedData node wrappers with the C
61
+ pointer and owning document embedded in one RVALUE, hot reads
62
+ (#name, #content, #[]/#attribute, #children, #element_children,
63
+ #next_sibling, #parent, #node_type) bound directly in C over the
64
+ dlsym-resolved library, and children constructed in bulk (one
65
+ wrapper-cache round-trip, zero Ruby frames, zero per-node
66
+ FFI::Pointer). Measured **~5x on raw walks** vs the binding path
67
+ and **~45 kB/doc held wrappers**; identity is shared with the
68
+ binding through the per-document wrapper cache, so both views
69
+ of one document interoperate. The default require path never
70
+ loads the compiled bundle; installs without it (ruby-platform
71
+ gem) get a clear LoadError. The ext ships vendored in platform
72
+ gems (no compile at install).
73
+ \n## [1.9.162.1] - 2026-09-14
9
74
 
10
75
  ### Performance
11
76
 
data/Rakefile CHANGED
@@ -85,6 +85,37 @@ task :compile do
85
85
  raise "utf8proc shared library not found after build" unless u8_lib
86
86
  cp(u8_lib, "lib/#{File.basename(u8_lib)}")
87
87
  puts "Vendored #{File.basename(lib)} + #{File.basename(u8_lib)} into lib/"
88
+
89
+ # Opt-in native read layer ext (#185): built and vendored beside
90
+ # the library — no compile at install; resolves libleptris at
91
+ # require time via dlsym.
92
+ ext_dir = "ext/leptris/native"
93
+ Dir.chdir(ext_dir) do
94
+ sh "ruby extconf.rb"
95
+ if RUBY_PLATFORM =~ /darwin/
96
+ # Link the bundle ourselves: setup-ruby's custom rubies make
97
+ # mkmf link libruby by absolute runner path no matter which
98
+ # RbConfig entries are cleared, and any libruby
99
+ # LC_LOAD_DYLIB is unresolvable on user machines. Compile
100
+ # via the Makefile, link with pure -undefined dynamic_lookup
101
+ # (Ruby symbols resolve from the loading interpreter) and a
102
+ # conservative deployment target.
103
+ sh "make native.o"
104
+ sh "cc -dynamic -bundle -undefined dynamic_lookup " \
105
+ "-mmacosx-version-min=#{ENV['MACOSX_DEPLOYMENT_TARGET'] || '11.0'} " \
106
+ "-o native.bundle native.o"
107
+ else
108
+ sh "make"
109
+ end
110
+ end
111
+ bundle = Dir.glob("#{ext_dir}/native.{bundle,so,dll}").first
112
+ raise "native layer bundle not found after build" unless bundle
113
+ if RUBY_PLATFORM =~ /darwin/
114
+ # Build-log proof of the linkage contract: libSystem only.
115
+ puts `otool -L #{bundle}`
116
+ end
117
+ cp(bundle, "lib/leptris/xml/#{File.basename(bundle)}")
118
+ puts "Vendored native layer (#{File.basename(bundle)}) into lib/leptris/xml/"
88
119
  end
89
120
 
90
121
  task spec: :compile unless ENV.key?("LEPTRIS_LIB_PATH")
@@ -189,6 +220,7 @@ platforms.each do |platform|
189
220
  spec.platform = Gem::Platform.new(platform)
190
221
  spec.files += Dir.glob("lib/libleptris.{dll,so,dylib}")
191
222
  spec.files += Dir.glob("lib/{libutf8proc.3.dylib,libutf8proc.so.3,libutf8proc.so,utf8proc.dll}")
223
+ spec.files += Dir.glob("lib/leptris/xml/native.{bundle,so,dll}")
192
224
  task = Gem::PackageTask.new(spec)
193
225
  task.define
194
226
  end
@@ -1,6 +1,11 @@
1
1
  # 21 — #147 option B: TypedData wrapper variant (owner greenlit)
2
2
 
3
- Status: PROTOTYPE DONE hypothesis VALIDATED (114 B/node measured)
3
+ Status: round 2 PASSED (2026-09-14) branch proto/native-nodes
4
+ is the reference implementation; build phase remains (full read
5
+ surface, seam, 9-platform packaging). Round 1 handle-level spike
6
+ (feat/typed-wrapper-variant, never merged) FAILED its gate — held
7
+ -8% vs walks +40% because per-call FFI::Pointer marshaling
8
+ dominates; the fix was C-binding the reads themselves.
4
9
 
5
10
  The greenlight arrived ("proceed with all"); the execution plan,
6
11
  in verification order:
@@ -24,3 +29,14 @@ in verification order:
24
29
  compilation per platform, packaging).
25
30
  - [ ] If NOT validated (win < ~15%): record the negative result on
26
31
  #147 and close option B with data.
32
+
33
+ ## Round 2 (2026-09-14): converged native nodes — PASSED
34
+
35
+ ext/leptris/native/native.c: TypedData wrappers (ptr + document
36
+ embedded), C-bound hot reads via dlsym (no FFI marshaling), bulk
37
+ children (one cache round-trip, zero Ruby frames). Measured: walk
38
+ 17.2 -> 2.8 ms (6.2x, interleaved best-of under load ~148 — ratio
39
+ conservative); held wrappers ~54 kB/doc (RSS, load-independent).
40
+ Identity shared through the binding wrapper_cache. Data on #185.
41
+ Remaining: full read surface, variant seam, compiled-ext
42
+ packaging (9-platform), release.
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The bundle must load into ANY Ruby (not the building runner's):
4
+ # macOS links with -undefined dynamic_lookup (Ruby symbols resolve
5
+ # from the loading interpreter — the standard C-ext convention)
6
+ # and a conservative deployment target; libruby is never linked.
7
+ ENV["MACOSX_DEPLOYMENT_TARGET"] ||= "11.0"
8
+ require "mkmf"
9
+ if RUBY_PLATFORM =~ /darwin/
10
+ # Robust across mkmf versions: older rubies drop $(ldflags)
11
+ # from the bundle link line, so pin BOTH the compile and link
12
+ # variables. -undefined dynamic_lookup is the standard C-ext
13
+ # convention (Ruby symbols resolve from the loading
14
+ # interpreter); -mmacosx-version-min keeps the bundle loadable
15
+ # on older user OSes than the building runner.
16
+ min = " -mmacosx-version-min=#{ENV['MACOSX_DEPLOYMENT_TARGET']}"
17
+ lookup = " -undefined dynamic_lookup"
18
+ [$LDFLAGS, $DLDFLAGS, $CFLAGS, $CXXFLAGS].each do |var|
19
+ var << min unless var.include?(min.strip)
20
+ end
21
+ [$LDFLAGS, $DLDFLAGS].each do |var|
22
+ var << lookup unless var.include?("dynamic_lookup")
23
+ end
24
+ # setup-ruby builds carry the RUNNER's absolute libruby path in
25
+ # LIBRUBYARG_SHARED — recorded as an LC_LOAD_DYLIB in the
26
+ # bundle, unresolvable on user machines. A Ruby extension
27
+ # resolves Ruby symbols from the loading interpreter; never
28
+ # link libruby.
29
+ # BOTH maps: mkmf's Makefile interpolates from MAKEFILE_CONFIG,
30
+ # not CONFIG — clearing only CONFIG is a silent no-op (caught via
31
+ # the build-log otool on the runner).
32
+ RbConfig::CONFIG["LIBRUBYARG_SHARED"] = ""
33
+ RbConfig::CONFIG["LIBRUBYARG_STATIC"] = ""
34
+ RbConfig::MAKEFILE_CONFIG["LIBRUBYARG_SHARED"] = ""
35
+ RbConfig::MAKEFILE_CONFIG["LIBRUBYARG_STATIC"] = ""
36
+ end
37
+ create_makefile("leptris/xml/native")
@@ -0,0 +1,295 @@
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 <stdint.h>
9
+ #include <string.h>
10
+
11
+ #ifdef _WIN32
12
+ #include <windows.h>
13
+ #else
14
+ #include <dlfcn.h>
15
+ #endif
16
+
17
+ typedef const char *(*elem_name_fn)(void *);
18
+ typedef const char *(*text_content_fn)(void *);
19
+ typedef const char *(*attr_fn)(void *, const char *);
20
+ typedef int (*children_ex_fn)(void *, void **, int *, int);
21
+ typedef int (*node_type_fn)(void *);
22
+ typedef void *(*next_sibling_fn)(void *);
23
+ typedef void *(*parent_fn)(void *);
24
+ typedef const char *(*element_text_fn)(void *);
25
+
26
+ static elem_name_fn f_elem_name;
27
+ static text_content_fn f_text_content;
28
+ static attr_fn f_attr;
29
+ static children_ex_fn f_children_ex;
30
+ static node_type_fn f_node_type;
31
+ static next_sibling_fn f_next_sibling;
32
+ static parent_fn f_parent;
33
+ static element_text_fn f_element_text;
34
+
35
+ static VALUE c_native_node;
36
+ static ID id_wrapper_cache;
37
+
38
+ #define NT_ELEMENT 0
39
+
40
+ struct native_node {
41
+ void *ptr;
42
+ VALUE document;
43
+ };
44
+
45
+ static void nn_mark(void *p)
46
+ {
47
+ struct native_node *n = p;
48
+ if (n->document != Qnil)
49
+ rb_gc_mark(n->document);
50
+ }
51
+
52
+ static size_t nn_size(const void *p) { (void)p; return sizeof(struct native_node); }
53
+
54
+ static const rb_data_type_t nn_type = {
55
+ "Leptris/XML/NativeNode",
56
+ { nn_mark, RUBY_TYPED_DEFAULT_FREE, nn_size, },
57
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
58
+ };
59
+
60
+ #ifdef _WIN32
61
+ static void *lib_open(const char *path) { return (void *)LoadLibraryA(path); }
62
+ static void *lib_sym(void *h, const char *name)
63
+ {
64
+ return (void *)GetProcAddress((HMODULE)h, name);
65
+ }
66
+ #else
67
+ static void *lib_open(const char *path) { return dlopen(path, RTLD_NOW); }
68
+ static void *lib_sym(void *h, const char *name) { return dlsym(h, name); }
69
+ #endif
70
+
71
+ static void resolve_symbols(const char *lib_path)
72
+ {
73
+ void *h = lib_open(lib_path);
74
+ if (!h)
75
+ rb_raise(rb_eRuntimeError, "cannot load library %s", lib_path);
76
+ f_elem_name = (elem_name_fn)lib_sym(h, "leptris_element_name");
77
+ f_text_content = (text_content_fn)lib_sym(h, "leptris_text_node_get_content");
78
+ f_attr = (attr_fn)lib_sym(h, "leptris_element_attribute");
79
+ f_children_ex = (children_ex_fn)lib_sym(h, "leptris_node_children_ex");
80
+ f_node_type = (node_type_fn)lib_sym(h, "leptris_node_get_type");
81
+ f_next_sibling = (next_sibling_fn)lib_sym(h, "leptris_node_next_sibling");
82
+ f_parent = (parent_fn)lib_sym(h, "leptris_node_parent");
83
+ f_element_text = (element_text_fn)lib_sym(h, "leptris_element_text");
84
+ if (!f_elem_name || !f_text_content || !f_attr ||
85
+ !f_children_ex || !f_node_type || !f_next_sibling || !f_parent ||
86
+ !f_element_text)
87
+ rb_raise(rb_eRuntimeError, "libleptris symbols missing");
88
+ }
89
+
90
+ static VALUE nn_name(VALUE self)
91
+ {
92
+ struct native_node *n;
93
+ const char *s;
94
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
95
+ s = f_elem_name(n->ptr);
96
+ return s ? rb_str_new_cstr(s) : Qnil;
97
+ }
98
+
99
+ static VALUE nn_content(VALUE self)
100
+ {
101
+ struct native_node *n;
102
+ const char *s;
103
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
104
+ /* elements aggregate their text (leptris_element_text); text
105
+ * nodes carry it directly. */
106
+ s = f_node_type(n->ptr) == 0 ? f_element_text(n->ptr)
107
+ : f_text_content(n->ptr);
108
+ return s ? rb_str_new_cstr(s) : Qnil;
109
+ }
110
+
111
+ static VALUE nn_attribute(VALUE self, VALUE name)
112
+ {
113
+ struct native_node *n;
114
+ const char *s;
115
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
116
+ s = f_attr(n->ptr, StringValueCStr(name));
117
+ return s ? rb_str_new_cstr(s) : Qnil;
118
+ }
119
+
120
+ static VALUE nn_allocate(VALUE klass)
121
+ {
122
+ struct native_node *n;
123
+ return TypedData_Make_Struct(klass, struct native_node, &nn_type, n);
124
+ }
125
+
126
+ /* Bulk children: one native pass; cache check/store inline; the
127
+ * document's wrapper_cache (a Ruby Hash keyed by address) provides
128
+ * identity for nodes seen through other paths. */
129
+ static VALUE nn_children(VALUE self)
130
+ {
131
+ struct native_node *n;
132
+ void *buf[512];
133
+ int kinds[512];
134
+ int count, i;
135
+ VALUE cache, out;
136
+
137
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
138
+ count = f_children_ex(n->ptr, buf, kinds, 512);
139
+ if (count <= 0)
140
+ return rb_ary_new2(0);
141
+
142
+ cache = rb_funcall(n->document, id_wrapper_cache, 0);
143
+ out = rb_ary_new2(count);
144
+ for (i = 0; i < count; i++) {
145
+ uint64_t addr = (uint64_t)(uintptr_t)buf[i];
146
+ VALUE key = ULL2NUM(addr);
147
+ VALUE child = rb_hash_aref(cache, key);
148
+ if (NIL_P(child)) {
149
+ struct native_node *cn;
150
+ child = nn_allocate(c_native_node);
151
+ TypedData_Get_Struct(child, struct native_node, &nn_type, cn);
152
+ cn->ptr = buf[i];
153
+ cn->document = n->document;
154
+ rb_hash_aset(cache, key, child);
155
+ }
156
+ rb_ary_push(out, child);
157
+ }
158
+ return out;
159
+ }
160
+
161
+ static VALUE nn_type_sym(VALUE self)
162
+ {
163
+ struct native_node *n;
164
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
165
+ switch (f_node_type(n->ptr)) {
166
+ case 0: return ID2SYM(rb_intern("element"));
167
+ case 1: return ID2SYM(rb_intern("text"));
168
+ case 2: return ID2SYM(rb_intern("comment"));
169
+ case 3: return ID2SYM(rb_intern("cdata"));
170
+ case 4: return ID2SYM(rb_intern("pi"));
171
+ default: return ID2SYM(rb_intern("node"));
172
+ }
173
+ }
174
+
175
+ /* Bulk children filtered to elements — the walk shape consumers
176
+ * actually iterate (#185's element_children parity). */
177
+ static VALUE nn_element_children(VALUE self)
178
+ {
179
+ struct native_node *n;
180
+ void *buf[512];
181
+ int kinds[512];
182
+ int count, i;
183
+ VALUE cache, out;
184
+
185
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
186
+ count = f_children_ex(n->ptr, buf, kinds, 512);
187
+ if (count <= 0)
188
+ return rb_ary_new2(0);
189
+
190
+ cache = rb_funcall(n->document, id_wrapper_cache, 0);
191
+ out = rb_ary_new();
192
+ for (i = 0; i < count; i++) {
193
+ if (kinds[i] != NT_ELEMENT)
194
+ continue;
195
+ VALUE key = ULL2NUM((uint64_t)(uintptr_t)buf[i]);
196
+ VALUE child = rb_hash_aref(cache, key);
197
+ if (NIL_P(child)) {
198
+ struct native_node *cn;
199
+ child = nn_allocate(c_native_node);
200
+ TypedData_Get_Struct(child, struct native_node, &nn_type, cn);
201
+ cn->ptr = buf[i];
202
+ cn->document = n->document;
203
+ rb_hash_aset(cache, key, child);
204
+ }
205
+ rb_ary_push(out, child);
206
+ }
207
+ return out;
208
+ }
209
+
210
+ static VALUE wrap_cached(struct native_node *owner, void *ptr)
211
+ {
212
+ VALUE cache, key, node;
213
+ struct native_node *n;
214
+
215
+ cache = rb_funcall(owner->document, id_wrapper_cache, 0);
216
+ key = ULL2NUM((uint64_t)(uintptr_t)ptr);
217
+ node = rb_hash_aref(cache, key);
218
+ if (!NIL_P(node))
219
+ return node;
220
+ node = nn_allocate(c_native_node);
221
+ TypedData_Get_Struct(node, struct native_node, &nn_type, n);
222
+ n->ptr = ptr;
223
+ n->document = owner->document;
224
+ rb_hash_aset(cache, key, node);
225
+ return node;
226
+ }
227
+
228
+ static VALUE nn_next_sibling(VALUE self)
229
+ {
230
+ struct native_node *n;
231
+ void *sib;
232
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
233
+ sib = f_next_sibling(n->ptr);
234
+ return sib ? wrap_cached(n, sib) : Qnil;
235
+ }
236
+
237
+ static VALUE nn_parent(VALUE self)
238
+ {
239
+ struct native_node *n;
240
+ void *par;
241
+ TypedData_Get_Struct(self, struct native_node, &nn_type, n);
242
+ par = f_parent(n->ptr);
243
+ return par ? wrap_cached(n, par) : Qnil;
244
+ }
245
+
246
+ static VALUE nn_from(VALUE klass, VALUE document, VALUE element)
247
+ {
248
+ /* Element address comes through the binding's #c_ptr address;
249
+ * registered in the shared identity cache so parent/sibling
250
+ * round-trips return the same object. */
251
+ struct native_node *n;
252
+ VALUE node = nn_allocate(klass);
253
+ void *ptr = (void *)(uintptr_t)NUM2ULL(
254
+ rb_funcall(element, rb_intern("address"), 0));
255
+ TypedData_Get_Struct(node, struct native_node, &nn_type, n);
256
+ n->ptr = ptr;
257
+ n->document = document;
258
+ rb_hash_aset(rb_funcall(document, id_wrapper_cache, 0),
259
+ ULL2NUM((uint64_t)(uintptr_t)ptr), node);
260
+ return node;
261
+ }
262
+
263
+ /* Ruby-side bootstrap: resolve the libleptris symbols once, from
264
+ * the path the FFI layer loaded. */
265
+ static VALUE leptris_native_resolve_rb(VALUE self, VALUE lib_path)
266
+ {
267
+ (void)self;
268
+ resolve_symbols(StringValueCStr(lib_path));
269
+ return Qnil;
270
+ }
271
+
272
+ void Init_native(void)
273
+ {
274
+ VALUE m_leptris, m_xml;
275
+
276
+ m_leptris = rb_define_module("Leptris");
277
+ m_xml = rb_define_module_under(m_leptris, "XML");
278
+ c_native_node = rb_define_class_under(m_xml, "NativeNode", rb_cObject);
279
+ rb_undef_alloc_func(c_native_node);
280
+
281
+ id_wrapper_cache = rb_intern("wrapper_cache");
282
+
283
+ rb_define_singleton_method(c_native_node, "from", nn_from, 2);
284
+ rb_define_method(c_native_node, "name", nn_name, 0);
285
+ rb_define_method(c_native_node, "content", nn_content, 0);
286
+ rb_define_method(c_native_node, "attribute", nn_attribute, 1);
287
+ rb_define_alias(c_native_node, "[]", "attribute");
288
+ rb_define_method(c_native_node, "children", nn_children, 0);
289
+ rb_define_method(c_native_node, "element_children", nn_element_children, 0);
290
+ rb_define_method(c_native_node, "next_sibling", nn_next_sibling, 0);
291
+ rb_define_method(c_native_node, "parent", nn_parent, 0);
292
+ rb_define_method(c_native_node, "node_type", nn_type_sym, 0);
293
+ rb_define_singleton_method(c_native_node, "resolve!",
294
+ leptris_native_resolve_rb, 1);
295
+ }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.162.1"
4
+ VERSION = "1.9.162.6"
5
5
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Opt-in native read layer (TODO.restructure/21 round 2, #185):
4
+ # TypedData node wrappers with C-bound hot reads and bulk children
5
+ # construction — measured 6.2x on raw walks. Load explicitly:
6
+ #
7
+ # require "leptris/xml/native"
8
+ # root = doc.native_node
9
+ # root.children.each { |c| c.name }
10
+ #
11
+ # Identity is shared with the binding through the per-document
12
+ # wrapper cache, so native and binding views of one document
13
+ # interoperate. The default require path never loads this file;
14
+ # installs without the compiled bundle (ruby-platform gem) raise
15
+ # a clear LoadError from the require.
16
+
17
+ begin
18
+ # The compiled bundle (feature "leptris/xml/native" resolves
19
+ # native.bundle). A sanctioned require exception: a .bundle
20
+ # cannot go through autoload. Missing in installs that do not
21
+ # vendor it (ruby-platform gem).
22
+ require "leptris/xml/native"
23
+ rescue LoadError => e
24
+ raise LoadError, <<~MSG
25
+ leptris: the compiled native read layer is not available in
26
+ this install (#{e.message}). It ships in platform gems; the
27
+ ruby-platform variant does not carry it.
28
+ MSG
29
+ end
30
+
31
+ module Leptris::XML::Native
32
+ LIB_CANDIDATES = [
33
+ ENV["LEPTRIS_LIB_PATH"],
34
+ File.expand_path("../../libleptris.dylib", __dir__),
35
+ File.expand_path("../../libleptris.so", __dir__),
36
+ File.expand_path("../../libleptris.dll", __dir__),
37
+ "libleptris",
38
+ ].compact.freeze
39
+ end
40
+
41
+ candidates = Leptris::XML::Native::LIB_CANDIDATES
42
+ resolved = candidates.any? do |path|
43
+ resolved = Leptris::XML::Native::LIB_CANDIDATES.any? do |path|
44
+ begin
45
+ Leptris::XML::NativeNode.resolve!(path)
46
+ true
47
+ rescue RuntimeError
48
+ false
49
+ end
50
+ end
51
+ raise LoadError,
52
+ "leptris: native layer loaded but libleptris could not be " \
53
+ "resolved from #{candidates.inspect}" unless resolved
54
+ end
55
+
56
+ class Leptris::XML::Document
57
+ # The native-node view of the document root (opt-in read layer).
58
+ def native_node
59
+ root or raise Leptris::XML::Error, "document has no root element"
60
+ Leptris::XML::NativeNode.from(self, root.c_ptr)
61
+ end
62
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.162.1
4
+ version: 1.9.162.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -97,6 +97,8 @@ files:
97
97
  - docs/adr/0004-lifetime-guard.md
98
98
  - docs/adr/0005-autoload-manifest-ordering.md
99
99
  - docs/adr/0006-ruby-variant-policy.md
100
+ - ext/leptris/native/extconf.rb
101
+ - ext/leptris/native/native.c
100
102
  - leptris.gemspec
101
103
  - lib/leptris.rb
102
104
  - lib/leptris/version.rb
@@ -117,6 +119,7 @@ files:
117
119
  - lib/leptris/xml/iteration_scope.rb
118
120
  - lib/leptris/xml/iterparse.rb
119
121
  - lib/leptris/xml/namespace.rb
122
+ - lib/leptris/xml/native_layer.rb
120
123
  - lib/leptris/xml/node.rb
121
124
  - lib/leptris/xml/node_set.rb
122
125
  - lib/leptris/xml/parse_options.rb