Module: Leptris::XML::FFI
- Extended by:
- FFI::Library
- Defined in:
- lib/leptris/xml/ffi.rb
Defined Under Namespace
Classes: ParseOptionsStruct, PullEventStruct, SAXHandler, SaxEventRecord, SerializeExtStruct, SerializeOptions
Constant Summary collapse
- SAX_EVENT_START_DOCUMENT =
0- SAX_EVENT_END_DOCUMENT =
1- SAX_EVENT_START_ELEMENT =
2- SAX_EVENT_END_ELEMENT =
3- SAX_EVENT_CHARACTERS =
4- SAX_EVENT_COMMENT =
5- SAX_EVENT_CDATA =
6- SAX_EVENT_PI =
7- SAX_EVENT_START_PREFIX =
8- SAX_EVENT_END_PREFIX =
9- SAX_EVENT_ERROR =
10- ITERPARSE_TOP_LEVEL =
0- ITERPARSE_FULL_DOCUMENT =
1- LEPTRIS_OK =
0- LEPTRIS_ERROR_MEMORY =
-1
- LEPTRIS_ERROR_PARSE =
-2
- LEPTRIS_ERROR_XPATH =
-3
- LEPTRIS_ERROR_NULL_ARG =
-4
- LEPTRIS_ERROR_INVALID_ARG =
-5
- LEPTRIS_ERROR_NOT_FOUND =
-6
- LEPTRIS_ERROR_IO =
-7
- LEPTRIS_ERROR_NOT_IMPLEMENTED =
-8
- XPATH_NODESET =
0- XPATH_BOOLEAN =
1- XPATH_NUMBER =
2- XPATH_STRING =
3- XPATH_NODE_ELEMENT =
0- XPATH_NODE_ATTRIBUTE =
1- XPATH_NODE_TEXT =
2- XPATH_NODE_OTHER =
3- NODE_ELEMENT =
0- NODE_TEXT =
1- NODE_COMMENT =
2- NODE_CDATA =
3- NODE_PI =
4- NODE_DOCTYPE =
5- NODE_ATTRIBUTE =
6- C14N_1_0 =
0- C14N_1_1 =
1- C14N_MODE_CANONICAL =
0- C14N_MODE_EXCLUSIVE =
1- TRAVERSE_PRE_ORDER =
0- TRAVERSE_POST_ORDER =
1- PULL_START_ELEMENT =
0- PULL_END_ELEMENT =
1- PULL_TEXT =
2- PULL_COMMENT =
3- PULL_CDATA =
4- PULL_PI =
5- PULL_END_DOCUMENT =
6- PULL_ERROR =
7- PULL_START_PREFIX =
Prefix events (types.h LeptrisPullEventType 8/9): the default namespace's prefix is legitimately "".
8- PULL_END_PREFIX =
9- LEPTRIS_PARSE_DEFAULT =
0- LEPTRIS_PARSE_DROP_WS_TEXT =
1- LEPTRIS_PARSE_DTDATTR =
libleptris >= 1.9.8 (#606): apply DTD ATTLIST default (and #FIXED) attribute values at parse time — libxml2 XML_PARSE_DTDATTR opt-in parity.
2- NS_SET_CACHE =
Namespace-binding lifecycle, written once: flatten the prefix/URI hash into the alternating CStringArray wire format and build the C set. Sets are cached per distinct vocabulary: the XPath VM reads the set as a const map during evaluation (vm.c) and never mutates it, so one set serves the process across queries and threads — building the wire format plus a C set per query cost ~6.7 µs, 2.6× a no-namespace query on a small document. Cached sets are never freed (bounded by the process's distinct namespace vocabularies — the same trade the CSS translation cache makes). Failed builds raise before caching.
{}
- XPATH_KIND_HINT =
XPath result-set batch (leptris_xpath_result_get_nodes_ex): the call fills out_kinds in the 4-value XPATH_NODE space (ELEMENT/ATTRIBUTE/TEXT/OTHER). ELEMENT is the only value that maps unambiguously — XPath's data model reports CDATA as TEXT, so a TEXT hint cannot distinguish Text from CDATA (different content getters) — everything else falls back to get_type in the wrapper. Returns [pointers, element_hints].
{ XPATH_NODE_ELEMENT => NODE_ELEMENT }.freeze
Class Method Summary collapse
-
.check_status(status) ⇒ Object
Single status seam: turns a C status code into a Ruby error, appending the library's last-error detail when present.
-
.fetch_attributes_raw(el_ptr) ⇒ Object
Raw qname-ordered attribute pairs INCLUDING xmlns at their byte positions (leptris_element_attributes_raw, 1.9.18 #635) — the streaming SAX contract's mixed view.
-
.fetch_children(c_ptr) ⇒ Object
Returns [pointers, kinds] — the kinds ride the batch (leptris_node_children_ex, 1.9.18 #617) so wrappers skip the per-child get_type dispatch.
-
.fetch_element_children(el_ptr) ⇒ Object
Element-only child batch (the mirror of fetch_children over leptris_element_children): element receivers skip wrapping — and paying for — the text/comment children they will filter out anyway; every wrap carries the ELEMENT hint so the per-child get_type dispatch disappears too.
- .fetch_result_nodes(result_ptr, count) ⇒ Object
- .ns_set_for(hash) ⇒ Object
-
.parse_fragment_with_status(xml, document_ptr) ⇒ Object
Fragment parse with the status out-param read: returns [document-or-fragment pointer, status].
-
.read_owned_string(ptr) ⇒ Object
Reads an libleptris-owned char* result and frees it as one unit, so a call site can neither leak nor double-free.
-
.read_pi_data(data) ⇒ Object
libxml2 consumes the whole whitespace run following the PI target when parsing; the engine retains it as data (leptris-ruby#85).
-
.scratch_bytes(need) ⇒ Object
Byte scratch for serialize-into cycles: the size+fill pair allocated and freed a buffer per call — inner_html serializes each child, and the allocation was most of the per-child cost.
-
.scratch_events(count) ⇒ Object
Pull-event staging block for leptris_pull_next_batch (the #589 bulk transport): LeptrisPullEvent records, stride from the struct layout that mirrors the ABI.
- .scratch_ints(count) ⇒ Object
- .scratch_pointers(count) ⇒ Object
-
.serialize_into_string(ffi_function, c_ptr, options) ⇒ Object
Caller-buffer serialization cycle: size query (buf=NULL), allocate exactly, fill, read.
- .status_message(status) ⇒ Object
-
.utf8(str) ⇒ Object
Attribute strings cross the seam UTF-8 on both faces (ADR-0002).
- .with_ns_set(hash) {|ns_set_for(hash)| ... } ⇒ Object
Class Method Details
.check_status(status) ⇒ Object
Single status seam: turns a C status code into a Ruby error, appending the library's last-error detail when present. The detail is a library-global string, so it can only be treated as best-effort context.
841 842 843 844 845 846 |
# File 'lib/leptris/xml/ffi.rb', line 841 def self.check_status(status) unless status == LEPTRIS_OK raise Leptris::XML::Error, (status) end status end |
.fetch_attributes_raw(el_ptr) ⇒ Object
Raw qname-ordered attribute pairs INCLUDING xmlns at their byte positions (leptris_element_attributes_raw, 1.9.18 #635) — the streaming SAX contract's mixed view.
968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 |
# File 'lib/leptris/xml/ffi.rb', line 968 def self.fetch_attributes_raw(el_ptr) total = leptris_element_attributes_raw(el_ptr, nil, nil, 0) return [] if total.zero? scratch = (Thread.current[:leptris_scratch] ||= {}) names = scratch[:attr_names] if names.nil? || names.size / PTR_BYTES < total names&.free names = scratch[:attr_names] = ::FFI::MemoryPointer.new(:pointer, total) end values = scratch[:attr_values] if values.nil? || values.size / PTR_BYTES < total values&.free values = scratch[:attr_values] = ::FFI::MemoryPointer.new(:pointer, total) end copied = leptris_element_attributes_raw( el_ptr, names, values, total) Array.new(copied) do |i| [utf8(names.get_pointer(i * PTR_BYTES).read_string), utf8(values.get_pointer(i * PTR_BYTES).read_string)] end end |
.fetch_children(c_ptr) ⇒ Object
Returns [pointers, kinds] — the kinds ride the batch (leptris_node_children_ex, 1.9.18 #617) so wrappers skip the per-child get_type dispatch.
931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 |
# File 'lib/leptris/xml/ffi.rb', line 931 def self.fetch_children(c_ptr) # One thread-local access serves both buffers (the helper # re-looked it up); the kinds buffer sizes from the POINTERS # buffer's capacity — it may have grown past FETCH_INITIAL on # earlier use, and C writes one kind per copied child. scratch = (Thread.current[:leptris_scratch] ||= {}) buffer = scratch[:pointers] if buffer.nil? buffer = scratch[:pointers] = ::FFI::MemoryPointer.new(:pointer, FETCH_INITIAL) end cap = buffer.size / PTR_BYTES kinds = scratch[:ints] if kinds.nil? || kinds.size / INT_BYTES < cap kinds&.free kinds = scratch[:ints] = ::FFI::MemoryPointer.new(:int, cap) end copied = leptris_node_children_ex(c_ptr, buffer, kinds, cap) if copied == cap total = leptris_node_children_ex(c_ptr, nil, nil, 0) if total > cap buffer.free buffer = scratch[:pointers] = ::FFI::MemoryPointer.new(:pointer, total) kinds.free kinds = scratch[:ints] = ::FFI::MemoryPointer.new(:int, total) copied = leptris_node_children_ex(c_ptr, buffer, kinds, total) end end [buffer.get_array_of_pointer(0, copied), kinds.get_array_of_int(0, copied)] end |
.fetch_element_children(el_ptr) ⇒ Object
Element-only child batch (the mirror of fetch_children over leptris_element_children): element receivers skip wrapping — and paying for — the text/comment children they will filter out anyway; every wrap carries the ELEMENT hint so the per-child get_type dispatch disappears too.
1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 |
# File 'lib/leptris/xml/ffi.rb', line 1003 def self.fetch_element_children(el_ptr) scratch = (Thread.current[:leptris_scratch] ||= {}) buffer = scratch[:pointers] if buffer.nil? buffer = scratch[:pointers] = ::FFI::MemoryPointer.new(:pointer, FETCH_INITIAL) end cap = buffer.size / PTR_BYTES copied = leptris_element_children(el_ptr, buffer, cap) if copied == cap # leptris_element_children has NO count-only mode (NULL # returns 0, unlike leptris_node_children) — size the # regrow from the dedicated counter. total = leptris_element_child_count(el_ptr) if total > cap buffer.free buffer = scratch[:pointers] = ::FFI::MemoryPointer.new(:pointer, total) copied = leptris_element_children(el_ptr, buffer, total) end end buffer.get_array_of_pointer(0, copied) end |
.fetch_result_nodes(result_ptr, count) ⇒ Object
1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 |
# File 'lib/leptris/xml/ffi.rb', line 1036 def self.fetch_result_nodes(result_ptr, count) return [[], nil] if count.zero? buffer = scratch_pointers(count) kinds = scratch_ints(count) copied = leptris_xpath_result_get_nodes_ex( result_ptr, buffer, kinds, count) hints = kinds.get_array_of_int(0, copied) .map { |k| XPATH_KIND_HINT[k] } [buffer.get_array_of_pointer(0, copied), hints] end |
.ns_set_for(hash) ⇒ Object
871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 |
# File 'lib/leptris/xml/ffi.rb', line 871 def self.ns_set_for(hash) pairs = hash.map { |prefix, uri| [prefix.to_s, uri.to_s] } key = if pairs.size == 1 "#{pairs[0][0]}\x00#{pairs[0][1]}" else pairs.sort.join("\x00") end NS_SET_CACHE.fetch(key) do flat = pairs.flatten buffer, _anchors = Leptris::XML::CStringArray.to_c(flat) set = leptris_xpath_ns_set_new_from_pairs(buffer, flat.length / 2) if set.null? raise Leptris::XML::Error, "leptris_xpath_ns_set_new_from_pairs failed" end NS_SET_CACHE[key] = set end end |
.parse_fragment_with_status(xml, document_ptr) ⇒ Object
Fragment parse with the status out-param read: returns [document-or-fragment pointer, status].
1049 1050 1051 1052 1053 1054 |
# File 'lib/leptris/xml/ffi.rb', line 1049 def self.parse_fragment_with_status(xml, document_ptr) status = ::FFI::MemoryPointer.new(:int) raw = leptris_parse_fragment( xml, xml.bytesize, document_ptr, status) [raw, status.read_int] end |
.read_owned_string(ptr) ⇒ Object
Reads an libleptris-owned char* result and frees it as one unit, so a call site can neither leak nor double-free.
831 832 833 834 835 |
# File 'lib/leptris/xml/ffi.rb', line 831 def self.read_owned_string(ptr) return "" if ptr.nil? || ptr.null? ptr.read_string.tap { leptris_free_string(ptr) } .force_encoding(Encoding::UTF_8) end |
.read_pi_data(data) ⇒ Object
libxml2 consumes the whole whitespace run following the PI target when parsing; the engine retains it as data (leptris-ruby#85). Every Ruby-facing PI-data read normalizes here — whitespace-only data becomes "" so "pi-without-data" PIs report empty consistently. Read-time only: PI#data= stores verbatim.
1062 1063 1064 |
# File 'lib/leptris/xml/ffi.rb', line 1062 def self.read_pi_data(data) data&.sub(/\A[ \t\r\n]+/, "") end |
.scratch_bytes(need) ⇒ Object
Byte scratch for serialize-into cycles: the size+fill pair allocated and freed a buffer per call — inner_html serializes each child, and the allocation was most of the per-child cost. Grows to the largest serialization seen; no Ruby reentrancy during a serialize call.
910 911 912 913 914 915 916 917 918 |
# File 'lib/leptris/xml/ffi.rb', line 910 def self.scratch_bytes(need) scratch = (Thread.current[:leptris_scratch] ||= {}) ptr = scratch[:bytes] if ptr.nil? || ptr.size < need ptr&.free ptr = scratch[:bytes] = ::FFI::MemoryPointer.new(:char, need) end ptr end |
.scratch_events(count) ⇒ Object
Pull-event staging block for leptris_pull_next_batch (the #589 bulk transport): LeptrisPullEvent records, stride from the struct layout that mirrors the ABI.
1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 |
# File 'lib/leptris/xml/ffi.rb', line 1103 def self.scratch_events(count) need = count * PullEventStruct.size scratch = (Thread.current[:leptris_scratch] ||= {}) ptr = scratch[:pull_events] if ptr.nil? || ptr.size < need ptr&.free ptr = scratch[:pull_events] = ::FFI::MemoryPointer.new(need) end ptr end |
.scratch_ints(count) ⇒ Object
1090 1091 1092 1093 1094 1095 1096 1097 1098 |
# File 'lib/leptris/xml/ffi.rb', line 1090 def self.scratch_ints(count) scratch = (Thread.current[:leptris_scratch] ||= {}) ptr = scratch[:ints] if ptr.nil? || ptr.size / INT_BYTES < count ptr&.free ptr = scratch[:ints] = ::FFI::MemoryPointer.new(:int, count) end ptr end |
.scratch_pointers(count) ⇒ Object
1080 1081 1082 1083 1084 1085 1086 1087 1088 |
# File 'lib/leptris/xml/ffi.rb', line 1080 def self.scratch_pointers(count) scratch = (Thread.current[:leptris_scratch] ||= {}) ptr = scratch[:pointers] if ptr.nil? || ptr.size / PTR_BYTES < count ptr&.free ptr = scratch[:pointers] = ::FFI::MemoryPointer.new(:pointer, count) end ptr end |
.serialize_into_string(ffi_function, c_ptr, options) ⇒ Object
Caller-buffer serialization cycle: size query (buf=NULL),
allocate exactly, fill, read. Since libleptris 1.9.0 the
pair reuses one serialization through the per-document
cache. ffi_function is leptris_document_serialize_into or
leptris_element_serialize_into. The buffer holds the
serialization + NUL and XML output is NUL-free, so the
bounded read is exact.
897 898 899 900 901 902 903 |
# File 'lib/leptris/xml/ffi.rb', line 897 def self.serialize_into_string(ffi_function, c_ptr, ) need = ffi_function.call(c_ptr, nil, 0, nil, ) return "" if need.zero? buffer = scratch_bytes(need) ffi_function.call(c_ptr, buffer, need, nil, ) buffer.read_string.force_encoding(Encoding::UTF_8) end |
.status_message(status) ⇒ Object
848 849 850 851 852 |
# File 'lib/leptris/xml/ffi.rb', line 848 def self.(status) base = leptris_status_string(status).to_s detail = leptris_last_error.to_s detail.empty? ? base : "#{base} (#{detail})" end |
.utf8(str) ⇒ Object
Attribute strings cross the seam UTF-8 on both faces (ADR-0002).
994 995 996 |
# File 'lib/leptris/xml/ffi.rb', line 994 def self.utf8(str) str.nil? ? nil : str.force_encoding(Encoding::UTF_8) end |
.with_ns_set(hash) {|ns_set_for(hash)| ... } ⇒ Object
867 868 869 |
# File 'lib/leptris/xml/ffi.rb', line 867 def self.with_ns_set(hash) yield ns_set_for(hash) end |