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 +4 -4
- data/CHANGELOG.md +66 -1
- data/Rakefile +32 -0
- data/TODO.restructure/21-typeddata-variant-prototype.md +17 -1
- data/ext/leptris/native/extconf.rb +37 -0
- data/ext/leptris/native/native.c +295 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/native_layer.rb +62 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d09354998670281f8e6519fa0cf7b35c467d67e7536879b31be2b3efa634250e
|
|
4
|
+
data.tar.gz: 362d92cf0e463160f94ecab7639a60d867680e47610bed4cac408dc0e835c1fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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:
|
|
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
|
+
}
|
data/lib/leptris/version.rb
CHANGED
|
@@ -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.
|
|
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
|