leptris 1.9.280.0 → 1.9.280.1
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 +57 -0
- data/ext/leptris/native/native.c +6 -0
- data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/ffi.rb +24 -40
- data/lib/leptris/xml/searchable.rb +14 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8c9a359271bb60e62c4a6fb8e210c725d02b332df53f804411498c82acd1056
|
|
4
|
+
data.tar.gz: 5e196178a59975549b08e1c3b54c8d5377d463d430ad8bceef5df410a5ea4e83
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4bbbdb0079f57fb234636a4ec03000a44a67ac6f80ea852fc4ba3bac9c0faddce6231900f137c8c9596369f4ea552120be792670da103f5169f2d796c891a56
|
|
7
|
+
data.tar.gz: 6a96c92cba79412fd69d32bc33482cd8b208808ded2b7d554539f7322647a9c6852e1531342fd428f7fe5826a94ccbfc428e4d094faca3d251dcdb2bddfb6aec
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
+
## [1.9.280.1] - 2026-09-30
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- Ractor support (#ractor): documents can be parsed, queried,
|
|
6
|
+
mutated, and GC-freed inside child ractors, end to end. Five
|
|
7
|
+
blockers cleared:
|
|
8
|
+
- The native ext declares `rb_ext_ractor_safe(true)` — it keeps
|
|
9
|
+
no unsynchronized shared mutable state (class caches are
|
|
10
|
+
GC-marked immortals; wrapper hashes mutate only along the
|
|
11
|
+
engine's per-document path).
|
|
12
|
+
- The UTF8_STRING converter is a MODULE, not a
|
|
13
|
+
Class.new instance: the instance form deadlocks the first
|
|
14
|
+
mapped conversion inside a child Ractor; singleton methods
|
|
15
|
+
on a module dispatch cleanly.
|
|
16
|
+
- The legacy `UTF8_RETURNS` re-wrapper is gone. It redefined
|
|
17
|
+
every string-returning attach through a `method(name)`
|
|
18
|
+
capture (`define_singleton_method` with a Proc defined in
|
|
19
|
+
the loading ractor) — exactly the shape that raises
|
|
20
|
+
"defined with an un-shareable Proc in a different Ractor" in
|
|
21
|
+
a child. The UTF8_STRING converter already tags those 26
|
|
22
|
+
functions' results UTF-8 (a double no-op), so the wrapper
|
|
23
|
+
was deleted, not ported.
|
|
24
|
+
- windows-11-arm only: ffi attaches aarch64-mingw-ucrt
|
|
25
|
+
functions through un-shareable Proc wrappers (its libffi
|
|
26
|
+
#905 workaround), breaking every attached call from a child
|
|
27
|
+
Ractor there. `Leptris::XML::FFI` now freezes itself on that
|
|
28
|
+
platform — ffi's `Library#freeze` redefines the wrappers via
|
|
29
|
+
`FFI.shareable_proc` (verified by a full suite run with the
|
|
30
|
+
freeze forced on; it is a no-op elsewhere). ffi's freeze
|
|
31
|
+
redemption itself needs `Ractor.shareable_proc` (Ruby 3.5+),
|
|
32
|
+
so on that platform with older Rubies the ractor specs skip
|
|
33
|
+
— attached calls from child ractors are impossible there
|
|
34
|
+
until the runtime catches up.
|
|
35
|
+
- `Searchable`'s compiled-expression and CSS translation
|
|
36
|
+
caches moved from module ivars to `Ractor.current[]`-keyed
|
|
37
|
+
storage: the old shape raises `Ractor::IsolationError` from
|
|
38
|
+
non-main ractors, the compiled FFI handles could never
|
|
39
|
+
cross a boundary anyway, and in the main ractor the sharing
|
|
40
|
+
is unchanged (one cache across its threads). Engines without
|
|
41
|
+
Ractor keep the ivar.
|
|
42
|
+
- Note for ractor users: a child ractor's autoload requests
|
|
43
|
+
are serviced by the main ractor, and a main blocked in
|
|
44
|
+
`Ractor#take` is not schedulable — under load that pairing can
|
|
45
|
+
wedge (CRuby ractor-barrier race). Exercise the API once in
|
|
46
|
+
the main ractor (or `Leptris::XML.parse` something trivial)
|
|
47
|
+
before spawning ractors against a cold load.
|
|
48
|
+
|
|
49
|
+
### Performance
|
|
50
|
+
|
|
51
|
+
- No measurable cost on the serial path: same attaches, same
|
|
52
|
+
converter, cache lookups trade one ivar read for a storage
|
|
53
|
+
read. Parallel parsing scales near-linearly instead — 4
|
|
54
|
+
ractors parse 4x20 documents in ~1/3 the serial time (0.06s
|
|
55
|
+
vs 0.18s on 4 cores).
|
|
56
|
+
|
|
57
|
+
|
|
1
58
|
## [1.9.279.0] - 2026-09-30
|
|
2
59
|
|
|
3
60
|
### Fixed
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -2937,6 +2937,12 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
|
|
|
2937
2937
|
{
|
|
2938
2938
|
VALUE m_leptris, m_xml, m_native;
|
|
2939
2939
|
|
|
2940
|
+
/* Ractor support: this ext keeps no unsynchronized shared mutable
|
|
2941
|
+
* state — class caches are GC-marked immortals (never freed), the
|
|
2942
|
+
* per-document wrapper hashes are keyed by document and every
|
|
2943
|
+
* entry mutates only under the engine's document path. */
|
|
2944
|
+
rb_ext_ractor_safe(true);
|
|
2945
|
+
|
|
2940
2946
|
m_leptris = rb_define_module("Leptris");
|
|
2941
2947
|
m_xml = rb_define_module_under(m_leptris, "XML");
|
|
2942
2948
|
c_native_node = rb_define_class_under(m_xml, "NativeNode", rb_cObject);
|
|
Binary file
|
|
Binary file
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -13,21 +13,30 @@ module Leptris
|
|
|
13
13
|
# therefore uses this converter: same NUL-terminated copy
|
|
14
14
|
# semantics as :string, tagged UTF-8. Input arguments keep
|
|
15
15
|
# plain :string (Ruby-to-C reads bytes regardless of tag).
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
# A MODULE, not a Class.new instance: the instance form
|
|
17
|
+
# (include DataConverter + tap) deadlocks the first mapped
|
|
18
|
+
# conversion inside a child Ractor, while singleton methods
|
|
19
|
+
# on a module dispatch cleanly (#ractor).
|
|
20
|
+
UTF8_STRING = Module.new do
|
|
21
|
+
extend ::FFI::DataConverter
|
|
22
|
+
|
|
23
|
+
def self.from_native(ptr, _ctx)
|
|
20
24
|
return nil if ptr.null? || ptr.address.zero?
|
|
21
25
|
|
|
22
26
|
ptr.read_string.force_encoding(Encoding::UTF_8)
|
|
23
27
|
end
|
|
24
28
|
|
|
25
|
-
def to_native(value, _ctx)
|
|
29
|
+
def self.to_native(value, _ctx)
|
|
26
30
|
return nil if value.nil?
|
|
27
31
|
|
|
28
32
|
::FFI::MemoryPointer.from_string(value.to_s)
|
|
29
33
|
end
|
|
30
|
-
|
|
34
|
+
|
|
35
|
+
def self.native_type(t = nil)
|
|
36
|
+
t ? (@native_type = t) : (@native_type ||= ::FFI::Type::POINTER)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
31
40
|
|
|
32
41
|
# The per-OS vendor directory of the ruby-platform gem
|
|
33
42
|
# (zero-setup TruffleRuby/JRuby, #160): binaries for the
|
|
@@ -1392,40 +1401,6 @@ attach_function :leptris_parse_string,
|
|
|
1392
1401
|
# XML_PARSE_DTDATTR opt-in parity.
|
|
1393
1402
|
LEPTRIS_PARSE_DTDATTR = 2
|
|
1394
1403
|
|
|
1395
|
-
# The headers' contract is UTF-8 for every C string, but FFI's
|
|
1396
|
-
# read_string hands back ASCII-8BIT — the platform default
|
|
1397
|
-
# leaks through the seam. Every string-returning attached
|
|
1398
|
-
# function this binding CALLS is wrapped here so its result
|
|
1399
|
-
# arrives as UTF-8; mirror-only attachments stay raw (add the
|
|
1400
|
-
# name here when a call site starts using its string).
|
|
1401
|
-
UTF8_RETURNS = %i[
|
|
1402
|
-
leptris_version
|
|
1403
|
-
leptris_status_string leptris_last_error
|
|
1404
|
-
leptris_document_last_error leptris_document_encoding
|
|
1405
|
-
leptris_document_pi_target leptris_document_pi_data
|
|
1406
|
-
leptris_document_comment_content
|
|
1407
|
-
leptris_element_name leptris_element_text leptris_element_prefix
|
|
1408
|
-
leptris_element_namespace
|
|
1409
|
-
leptris_element_attribute leptris_element_attribute_ns
|
|
1410
|
-
leptris_attribute_get_name leptris_attribute_get_value
|
|
1411
|
-
leptris_attribute_namespace_uri
|
|
1412
|
-
leptris_element_namespace_decl_prefix
|
|
1413
|
-
leptris_element_namespace_decl_uri
|
|
1414
|
-
leptris_text_node_get_content leptris_comment_node_get_content
|
|
1415
|
-
leptris_cdata_node_get_content
|
|
1416
|
-
leptris_pi_node_get_target leptris_pi_node_get_data
|
|
1417
|
-
leptris_pull_attr_name leptris_pull_attr_value
|
|
1418
|
-
].freeze
|
|
1419
|
-
private_constant :UTF8_RETURNS
|
|
1420
|
-
|
|
1421
|
-
UTF8_RETURNS.each do |name|
|
|
1422
|
-
raw = method(name)
|
|
1423
|
-
define_singleton_method(name) do |*args|
|
|
1424
|
-
str = raw.call(*args)
|
|
1425
|
-
str.nil? ? nil : str.force_encoding(Encoding::UTF_8)
|
|
1426
|
-
end
|
|
1427
|
-
end
|
|
1428
|
-
|
|
1429
1404
|
# Reads an libleptris-owned char* result and frees it as one unit,
|
|
1430
1405
|
# so a call site can neither leak nor double-free.
|
|
1431
1406
|
def self.read_owned_string(ptr)
|
|
@@ -1731,6 +1706,15 @@ attach_function :leptris_parse_string,
|
|
|
1731
1706
|
end
|
|
1732
1707
|
ptr
|
|
1733
1708
|
end
|
|
1709
|
+
|
|
1710
|
+
# ffi attaches aarch64-mingw-ucrt functions through un-shareable
|
|
1711
|
+
# Proc wrappers (the libffi #905 workaround in ffi/function.rb),
|
|
1712
|
+
# which breaks every attached call from a child Ractor on
|
|
1713
|
+
# windows-11-arm. Library#freeze redefines those wrappers via
|
|
1714
|
+
# FFI.shareable_proc — the ffi gem's own Ractor redemption path
|
|
1715
|
+
# for this platform (#ractor). Everything is attached above, at
|
|
1716
|
+
# load time, so freezing closes no live door.
|
|
1717
|
+
freeze if RUBY_PLATFORM == "aarch64-mingw-ucrt"
|
|
1734
1718
|
end
|
|
1735
1719
|
end
|
|
1736
1720
|
end
|
|
@@ -14,8 +14,20 @@ module Leptris::XML::Searchable
|
|
|
14
14
|
# evaluations keep their dedicated string entries.
|
|
15
15
|
COMPILED_CACHE_LIMIT = 64
|
|
16
16
|
|
|
17
|
+
# Cache storage (Ractor): module/class ivars are off-limits to
|
|
18
|
+
# non-main ractors (Ractor::IsolationError), and the compiled
|
|
19
|
+
# handles are FFI pointers that can never cross a ractor boundary
|
|
20
|
+
# anyway. Ractor#[] storage preserves the old sharing shape in
|
|
21
|
+
# the main ractor (one cache across its threads) and gives every
|
|
22
|
+
# child ractor its own. Engines without Ractor keep the ivar.
|
|
23
|
+
def self.cache_store(name)
|
|
24
|
+
return (@cache_stores ||= {})[name] ||= {} unless defined?(Ractor)
|
|
25
|
+
|
|
26
|
+
Ractor.current[name] ||= {}
|
|
27
|
+
end
|
|
28
|
+
|
|
17
29
|
def self.compiled_expression(expr)
|
|
18
|
-
cache = (
|
|
30
|
+
cache = cache_store(:compiled_expressions)
|
|
19
31
|
if (hit = cache[expr])
|
|
20
32
|
cache.delete(expr)
|
|
21
33
|
cache[expr] = hit # LRU refresh
|
|
@@ -34,7 +46,7 @@ module Leptris::XML::Searchable
|
|
|
34
46
|
# it every css call re-ran the regex machinery before the
|
|
35
47
|
# compiled-expression cache could hit. Same bounded-LRU shape.
|
|
36
48
|
def self.css_expression(selector, prefix)
|
|
37
|
-
cache = (
|
|
49
|
+
cache = cache_store(:css_expressions)
|
|
38
50
|
key = "#{prefix}#{selector}"
|
|
39
51
|
if (hit = cache[key])
|
|
40
52
|
cache.delete(key)
|