etassia-native 0.1.0
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 +7 -0
- data/lib/etassia/native/version.rb +7 -0
- data/lib/etassia/native.rb +172 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ad82fe8761289ebe9e5a7774a68667f747318cfc6c07eaedaa5a459b15bdcdef
|
|
4
|
+
data.tar.gz: d09df242a26ebbbe0019de672709091a8af7ef28b6890467bb551654ea3c8d77
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 654196fe987e74cbedd99d6edb3e34113c495aeeb373331bccb0a8bee3f040d46155119f0010ce2867489f3a7ed3cd08bc7e82c35c9d1205e2e3cd22c67c1ddf
|
|
7
|
+
data.tar.gz: 84080656b0b0eb0c44d8b7c95c788d25d69dbf30f57362eca88b948c3aa98402209b52bdda4731f30044e5b3e159d892c3af687bf403619e5d7b40388e649e73
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "ffi"
|
|
6
|
+
|
|
7
|
+
module Etassia
|
|
8
|
+
# Renders through the etassia-ffi C ABI (see crates/etassia-ffi/README.md).
|
|
9
|
+
#
|
|
10
|
+
# The shared library is looked up in this order:
|
|
11
|
+
# 1. ENV["ETASSIA_NATIVE_LIB"] — exact file path; when set, nothing else is searched.
|
|
12
|
+
# 2. lib/etassia/native/ inside an installed platform gem (precompiled, no compile at install).
|
|
13
|
+
# 3. <repo>/target/release, then <repo>/target/debug.
|
|
14
|
+
# When no library is found, or the Rust core answers `Unported`, render
|
|
15
|
+
# falls back to a pure-Ruby callable (default: the `etassia` gem's Etassia.render).
|
|
16
|
+
module Native
|
|
17
|
+
LIBRARY_ENV = "ETASSIA_NATIVE_LIB"
|
|
18
|
+
REPO_ROOT = File.expand_path("../../../..", __dir__)
|
|
19
|
+
PACKAGED_DIR = File.join(__dir__, "native").freeze
|
|
20
|
+
SEARCH_DIRS = [PACKAGED_DIR,
|
|
21
|
+
File.join(REPO_ROOT, "target", "release"),
|
|
22
|
+
File.join(REPO_ROOT, "target", "debug")].freeze
|
|
23
|
+
|
|
24
|
+
# The Rust core declined this diagram type; the caller should fall back.
|
|
25
|
+
class Unported < StandardError; end
|
|
26
|
+
|
|
27
|
+
# Any other error the native library reports (parse, encoding, ...). Never falls back.
|
|
28
|
+
class RenderError < StandardError
|
|
29
|
+
attr_reader :error_class, :line, :col
|
|
30
|
+
|
|
31
|
+
def initialize(error_class, message, line, col)
|
|
32
|
+
@error_class = error_class
|
|
33
|
+
@line = line
|
|
34
|
+
@col = col
|
|
35
|
+
super("#{error_class} at #{line}:#{col}: #{message}")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Neither the native library nor the pure-Ruby fallback can render.
|
|
40
|
+
class Unavailable < StandardError; end
|
|
41
|
+
|
|
42
|
+
# Loads one cdylib and calls its three entry points.
|
|
43
|
+
class Library
|
|
44
|
+
# Matches `struct EtassiaBuf` in crates/etassia-ffi/src/lib.rs (24 bytes on 64-bit).
|
|
45
|
+
class Buf < FFI::Struct
|
|
46
|
+
layout :ptr, :pointer,
|
|
47
|
+
:len, :size_t,
|
|
48
|
+
:status, :int
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
attr_reader :path
|
|
52
|
+
|
|
53
|
+
def initialize(path)
|
|
54
|
+
@path = path
|
|
55
|
+
buf = Buf
|
|
56
|
+
@lib = Module.new do
|
|
57
|
+
extend FFI::Library
|
|
58
|
+
|
|
59
|
+
ffi_lib path
|
|
60
|
+
attach_function :etassia_render, %i[pointer size_t pointer], buf.by_value
|
|
61
|
+
attach_function :etassia_detect, %i[pointer size_t], buf.by_value
|
|
62
|
+
attach_function :etassia_free, [buf.by_value], :void
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def render(source)
|
|
67
|
+
ptr, len = input(source)
|
|
68
|
+
take(@lib.etassia_render(ptr, len, nil))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def detect(source)
|
|
72
|
+
ptr, len = input(source)
|
|
73
|
+
take(@lib.etassia_detect(ptr, len))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def input(source)
|
|
79
|
+
bytes = source.to_s.encode(Encoding::UTF_8).b
|
|
80
|
+
ptr = FFI::MemoryPointer.new(:uint8, [bytes.bytesize, 1].max)
|
|
81
|
+
ptr.put_bytes(0, bytes)
|
|
82
|
+
[ptr, bytes.bytesize]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Copies the buffer out, always frees it, then maps status to result or error.
|
|
86
|
+
def take(buf)
|
|
87
|
+
body = buf[:ptr].read_bytes(buf[:len]).force_encoding(Encoding::UTF_8)
|
|
88
|
+
status = buf[:status]
|
|
89
|
+
@lib.etassia_free(buf)
|
|
90
|
+
return body if status.zero?
|
|
91
|
+
|
|
92
|
+
raise_error(body)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def raise_error(json)
|
|
96
|
+
info = JSON.parse(json)
|
|
97
|
+
raise Unported, info["message"] if info["class"] == "Unported"
|
|
98
|
+
|
|
99
|
+
raise RenderError.new(info["class"], info["message"], info["line"], info["col"])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class << self
|
|
104
|
+
# Pure-Ruby renderer used when the native path cannot answer. Injectable.
|
|
105
|
+
attr_writer :fallback
|
|
106
|
+
|
|
107
|
+
def fallback
|
|
108
|
+
@fallback ||= method(:pure_ruby_render)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Path of the library that would be loaded, or nil.
|
|
112
|
+
def library_path
|
|
113
|
+
override = ENV.fetch(LIBRARY_ENV, nil)
|
|
114
|
+
return (File.file?(override) ? override : nil) if override && !override.empty?
|
|
115
|
+
|
|
116
|
+
SEARCH_DIRS.map { |dir| File.join(dir, library_filename) }.find { |path| File.file?(path) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def available?
|
|
120
|
+
!library_path.nil?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# SVG for `source`. Uses the native library, or the fallback when it is
|
|
124
|
+
# missing or reports Unported. Other native errors raise RenderError.
|
|
125
|
+
def render(source, fallback: self.fallback)
|
|
126
|
+
lib = library
|
|
127
|
+
return fallback.call(source) if lib.nil?
|
|
128
|
+
|
|
129
|
+
lib.render(source)
|
|
130
|
+
rescue Unported
|
|
131
|
+
fallback.call(source)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Diagram type name for `source`. Native only; raises Unavailable without a library.
|
|
135
|
+
def detect(source)
|
|
136
|
+
lib = library or raise Unavailable,
|
|
137
|
+
"#{LIBRARY_ENV} not set and no #{library_filename} in the gem or under target/"
|
|
138
|
+
|
|
139
|
+
lib.detect(source)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def library
|
|
143
|
+
path = library_path
|
|
144
|
+
return nil if path.nil?
|
|
145
|
+
|
|
146
|
+
@libraries ||= {}
|
|
147
|
+
@libraries[path] ||= Library.new(path)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def library_filename
|
|
153
|
+
case RbConfig::CONFIG["host_os"]
|
|
154
|
+
when /darwin/ then "libetassia_ffi.dylib"
|
|
155
|
+
when /mswin|mingw/ then "etassia_ffi.dll"
|
|
156
|
+
else "libetassia_ffi.so"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def pure_ruby_render(source)
|
|
161
|
+
begin
|
|
162
|
+
require "etassia"
|
|
163
|
+
rescue LoadError
|
|
164
|
+
raise Unavailable, "native library not found and the pure-Ruby 'etassia' gem is not installed"
|
|
165
|
+
end
|
|
166
|
+
raise Unavailable, "the 'etassia' gem does not define Etassia.render" unless Etassia.respond_to?(:render)
|
|
167
|
+
|
|
168
|
+
Etassia.render(source)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: etassia-native
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Etassia contributors
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ffi
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.16'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.16'
|
|
26
|
+
executables: []
|
|
27
|
+
extensions: []
|
|
28
|
+
extra_rdoc_files: []
|
|
29
|
+
files:
|
|
30
|
+
- lib/etassia/native.rb
|
|
31
|
+
- lib/etassia/native/version.rb
|
|
32
|
+
licenses:
|
|
33
|
+
- BSD-2-Clause
|
|
34
|
+
metadata:
|
|
35
|
+
rubygems_mfa_required: 'true'
|
|
36
|
+
rdoc_options: []
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '3.1'
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubygems_version: 4.0.23
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: Ruby binding to the etassia-ffi C ABI, with a pure-Ruby fallback
|
|
53
|
+
test_files: []
|