spinel_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/CHANGELOG.md +10 -0
- data/LICENSE +21 -0
- data/README.md +115 -0
- data/examples/demo.rb +92 -0
- data/lib/spinel/native/builder.rb +196 -0
- data/lib/spinel/native/registry.rb +124 -0
- data/lib/spinel/native/source.rb +34 -0
- data/lib/spinel/native/types.rb +93 -0
- data/lib/spinel/native/version.rb +7 -0
- data/lib/spinel/native.rb +109 -0
- metadata +70 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 341a37c7333aabb9a5ca73e8bfc1dfeea3949c9629fdc809c81c34ee83369c43
|
|
4
|
+
data.tar.gz: d5a28eafd41ffe17e21c5fee4af88807f1e7103209a163a8e78a88e23d25a6f7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 79161e43d39d1d61384f14675c2b732c21c2b4d34f7ca0e38760ce929c339b04e51b0818d306c83bb74698e03b7bd447c42f1e4f257381a6e549090c5ac2b95c
|
|
7
|
+
data.tar.gz: cf96d9b21b54cb6a1adc910caceeced6aa8f14c766b2a70624ec9b13b1775918ba20e1551f154480ae066ba5ebc6ead092bba4d0bdd3d2ebfcffc7492cad9306
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- `native def` marks a method; its first call samples the argument types,
|
|
6
|
+
compiles the kernel with `spinel --ext cruby`, and rebinds the method.
|
|
7
|
+
- `native "(T, ...) -> R"` declares the types instead; `Spinel::Native.compile!`
|
|
8
|
+
builds at boot.
|
|
9
|
+
- `SPINEL_NATIVE=off|verify|strict` modes; the Ruby definition stays as
|
|
10
|
+
fallback and oracle.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Hasiński
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# spinel_native
|
|
2
|
+
|
|
3
|
+
Compile a single Ruby method to native code with the
|
|
4
|
+
[Spinel](https://github.com/matz/spinel) AOT compiler, from inside a running
|
|
5
|
+
CRuby program. Mark the hot method, keep everything else on CRuby.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require "spinel/native"
|
|
9
|
+
|
|
10
|
+
module Physics
|
|
11
|
+
extend Spinel::Native
|
|
12
|
+
|
|
13
|
+
native def dot(a, b)
|
|
14
|
+
s = 0.0
|
|
15
|
+
i = 0
|
|
16
|
+
while i < a.length
|
|
17
|
+
s += a[i] * b[i]
|
|
18
|
+
i += 1
|
|
19
|
+
end
|
|
20
|
+
s
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Physics.dot(xs, ys) # first call: compile + rebind (about 1s, cached on disk)
|
|
25
|
+
Physics.dot(xs, ys) # native
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That is the whole interface: `native def`. The method stays plain Ruby until
|
|
29
|
+
its first call. The argument types of that call seed Spinel's whole-program
|
|
30
|
+
type inference, the kernel is compiled into a CRuby extension, and the method
|
|
31
|
+
is rebound to the compiled entry. If anything goes wrong the Ruby definition
|
|
32
|
+
stays in place and a warning says why.
|
|
33
|
+
|
|
34
|
+
## Interface
|
|
35
|
+
|
|
36
|
+
- `native def name(args)` marks a method. In a module it is also callable as
|
|
37
|
+
`Mod.name` (it can never use `self`, so the distinction is moot). `native
|
|
38
|
+
def self.name` works too, and so do instance methods of a class.
|
|
39
|
+
- `native "(Array[Float], Integer) -> Float"` on the line before a `def`
|
|
40
|
+
declares the parameter types instead of sampling them. The return type is
|
|
41
|
+
inferred by Spinel and only checked for shape.
|
|
42
|
+
- `Spinel::Native.compile!(Mod)` compiles every declared entry now, at boot,
|
|
43
|
+
so compile errors surface before the first request.
|
|
44
|
+
- `SPINEL_NATIVE=off` runs the Ruby definitions only. `verify` runs both paths
|
|
45
|
+
on every call and raises `Spinel::Native::Mismatch` when they disagree (the
|
|
46
|
+
Ruby definition is the oracle). `strict` turns the silent fallback into a
|
|
47
|
+
raised `CompileError` / `TypeError`. Same knob: `Spinel::Native.mode = :verify`.
|
|
48
|
+
- `SPINEL=/path/to/spinel` names the compiler (otherwise `spinel` on PATH),
|
|
49
|
+
`SPINEL_NATIVE_CACHE` the build cache (default `~/.cache/spinel-native`),
|
|
50
|
+
`SPINEL_NATIVE_VERBOSE=1` prints the commands and timings.
|
|
51
|
+
|
|
52
|
+
## Rules for a native method
|
|
53
|
+
|
|
54
|
+
- Parameters and the return value cross the boundary **by copy**. Supported
|
|
55
|
+
types: `Integer` (64-bit), `Float`, `String`, `bool`, and `Array` of those.
|
|
56
|
+
Mutating a parameter is refused at compile time; return the result instead.
|
|
57
|
+
- The body must not touch `self`, instance variables, or anything outside the
|
|
58
|
+
module. Native methods may call each other; everything reachable must be
|
|
59
|
+
`native` too, because the kernel is exactly the set of marked methods.
|
|
60
|
+
- A `raise` inside the kernel arrives in Ruby as the same exception class and
|
|
61
|
+
message. Integer overflow raises `RangeError` where CRuby would promote to a
|
|
62
|
+
Bignum, and a Bignum argument is a `RangeError` at the boundary.
|
|
63
|
+
- The kernel runs without the GVL, one call at a time per module.
|
|
64
|
+
|
|
65
|
+
## How it works
|
|
66
|
+
|
|
67
|
+
1. `Method#source_location` plus Prism pull the `def` back out of its file.
|
|
68
|
+
2. The marked methods become `def self.` methods of a synthetic module, with
|
|
69
|
+
a `if __FILE__ == $0` driver that calls each exported entry once with a
|
|
70
|
+
literal of its parameter types (`[0.0]`, `0`, `"x"`). Spinel infers from
|
|
71
|
+
that call site and never runs the driver.
|
|
72
|
+
3. `spinel kernel.rb -c --ext cruby --ext-init ... --ext-entry Mod.a,Mod.b`
|
|
73
|
+
emits the kernel C, a header contract, and a CRuby shim that converts
|
|
74
|
+
`VALUE`s, releases the GVL, and re-raises kernel exceptions.
|
|
75
|
+
4. The C compiler from `RbConfig` links those with the Spinel runtime into a
|
|
76
|
+
shared object, keyed in the cache by the kernel source, the entries, the
|
|
77
|
+
Spinel binary and the Ruby ABI. The runtime itself is compiled once per
|
|
78
|
+
Spinel build with `-fPIC` (a couple of seconds), since the archive Spinel
|
|
79
|
+
ships is meant for executables. On Linux the object exports only its
|
|
80
|
+
`Init_*` symbol and binds the rest internally, because CRuby loads
|
|
81
|
+
extensions `RTLD_GLOBAL` and two kernels would otherwise share one
|
|
82
|
+
`sp_raise_cls`. `require` loads the object; the method is redefined to
|
|
83
|
+
forward to the extension.
|
|
84
|
+
|
|
85
|
+
## Install
|
|
86
|
+
|
|
87
|
+
Spinel is not on RubyGems; build it from source once and point the gem at it:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
git clone https://github.com/matz/spinel && cd spinel && make deps && make
|
|
91
|
+
export SPINEL=$PWD/bin/spinel
|
|
92
|
+
gem install spinel_native # or: gem "spinel_native" in the Gemfile
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Running the example
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
git clone https://github.com/khasinski/spinel_native && cd spinel_native
|
|
99
|
+
ruby -I lib examples/demo.rb
|
|
100
|
+
bundle exec rake test
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
On an M-series Mac with CRuby master the demo prints roughly:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
dot(2M floats) ruby 0.136s native 0.016s x8.4
|
|
107
|
+
mandel_row(4000) ruby 0.517s native 0.013s x39.6
|
|
108
|
+
count_primes(5M) ruby 0.645s native 0.072s x9.0
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Status
|
|
112
|
+
|
|
113
|
+
A proof of concept. Not yet done: shipping the compiled kernel inside a gem
|
|
114
|
+
(Spinel's own `spin ext` covers that path), zero-copy numeric buffers, keyword
|
|
115
|
+
and block parameters, Hash parameters, and `--int-overflow=promote` parity.
|
data/examples/demo.rb
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ruby -I lib examples/demo.rb
|
|
4
|
+
require "benchmark"
|
|
5
|
+
require "spinel/native"
|
|
6
|
+
|
|
7
|
+
module Physics
|
|
8
|
+
extend Spinel::Native
|
|
9
|
+
|
|
10
|
+
# Types are sampled from the first call.
|
|
11
|
+
native def dot(a, b)
|
|
12
|
+
s = 0.0
|
|
13
|
+
i = 0
|
|
14
|
+
while i < a.length
|
|
15
|
+
s += a[i] * b[i]
|
|
16
|
+
i += 1
|
|
17
|
+
end
|
|
18
|
+
s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Or declared, so the kernel can be compiled before the first call.
|
|
22
|
+
native "(Float, Float, Integer) -> Integer"
|
|
23
|
+
def mandel(cr, ci, limit)
|
|
24
|
+
zr = 0.0
|
|
25
|
+
zi = 0.0
|
|
26
|
+
n = 0
|
|
27
|
+
while n < limit && zr * zr + zi * zi < 4.0
|
|
28
|
+
t = zr * zr - zi * zi + cr
|
|
29
|
+
zi = 2.0 * zr * zi + ci
|
|
30
|
+
zr = t
|
|
31
|
+
n += 1
|
|
32
|
+
end
|
|
33
|
+
n
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Native methods may call each other; everything reachable must be native too.
|
|
37
|
+
native def mandel_row(ci, width, limit)
|
|
38
|
+
out = []
|
|
39
|
+
x = 0
|
|
40
|
+
while x < width
|
|
41
|
+
out << mandel(-2.0 + 3.0 * x / width, ci, limit)
|
|
42
|
+
x += 1
|
|
43
|
+
end
|
|
44
|
+
out
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class Sieve
|
|
49
|
+
extend Spinel::Native
|
|
50
|
+
|
|
51
|
+
# Instance methods work too; they must not touch ivars or self.
|
|
52
|
+
native def count_primes(n)
|
|
53
|
+
flags = Array.new(n + 1, true)
|
|
54
|
+
count = 0
|
|
55
|
+
i = 2
|
|
56
|
+
while i <= n
|
|
57
|
+
if flags[i]
|
|
58
|
+
count += 1
|
|
59
|
+
j = i * i
|
|
60
|
+
while j <= n
|
|
61
|
+
flags[j] = false
|
|
62
|
+
j += i
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
i += 1
|
|
66
|
+
end
|
|
67
|
+
count
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
a = Array.new(2_000_000) { |i| i * 0.5 }
|
|
72
|
+
|
|
73
|
+
Spinel::Native.mode = :off
|
|
74
|
+
t_ruby = Benchmark.realtime { 3.times { Physics.dot(a, a) } } / 3
|
|
75
|
+
p_ruby = Benchmark.realtime { Physics.mandel_row(0.1, 4000, 2000) }
|
|
76
|
+
s_ruby = Benchmark.realtime { Sieve.new.count_primes(5_000_000) }
|
|
77
|
+
|
|
78
|
+
Spinel::Native.mode = :on
|
|
79
|
+
Physics.dot(a, a) # first call compiles (or loads from cache)
|
|
80
|
+
Physics.mandel_row(0.1, 4, 10)
|
|
81
|
+
Sieve.new.count_primes(10)
|
|
82
|
+
t_nat = Benchmark.realtime { 3.times { Physics.dot(a, a) } } / 3
|
|
83
|
+
p_nat = Benchmark.realtime { Physics.mandel_row(0.1, 4000, 2000) }
|
|
84
|
+
s_nat = Benchmark.realtime { Sieve.new.count_primes(5_000_000) }
|
|
85
|
+
|
|
86
|
+
puts "dot(2M floats) ruby %.3fs native %.3fs x%.1f" % [t_ruby, t_nat, t_ruby / t_nat]
|
|
87
|
+
puts "mandel_row(4000) ruby %.3fs native %.3fs x%.1f" % [p_ruby, p_nat, p_ruby / p_nat]
|
|
88
|
+
puts "count_primes(5M) ruby %.3fs native %.3fs x%.1f" % [s_ruby, s_nat, s_ruby / s_nat]
|
|
89
|
+
Spinel::Native.mode = :off
|
|
90
|
+
ruby_dot = Physics.dot(a, a)
|
|
91
|
+
Spinel::Native.mode = :on
|
|
92
|
+
puts "results agree: #{Physics.dot(a, a) == ruby_dot && Sieve.new.count_primes(100) == 25 && Physics.mandel(0.0, 0.0, 50) == 50}"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "etc"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "shellwords"
|
|
6
|
+
|
|
7
|
+
module Spinel
|
|
8
|
+
module Native
|
|
9
|
+
# Turns a kernel (a module of `def self.` methods plus a witness driver)
|
|
10
|
+
# into a loaded CRuby extension:
|
|
11
|
+
#
|
|
12
|
+
# spinel kernel.rb -c --ext cruby --ext-init ... --ext-entry Mod.a,Mod.b
|
|
13
|
+
# cc -bundle kernel.c kernel_ext.c libspinel_rt.a
|
|
14
|
+
# require kernel.bundle
|
|
15
|
+
#
|
|
16
|
+
# Builds are cached under SPINEL_NATIVE_CACHE (default ~/.cache/spinel-native)
|
|
17
|
+
# keyed by the kernel source, the exported entries, the spinel binary and
|
|
18
|
+
# the Ruby ABI, so a repeated run of the same program compiles nothing.
|
|
19
|
+
class Builder
|
|
20
|
+
Result = Struct.new(:module_name, :bundle, :dir, :cached, :seconds)
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def spinel_bin
|
|
24
|
+
@spinel_bin ||= begin
|
|
25
|
+
env = ENV["SPINEL"].to_s
|
|
26
|
+
found = env.empty? ? which("spinel") : env
|
|
27
|
+
raise Error, "spinel compiler not found: set SPINEL=/path/to/spinel or put it on PATH" if found.to_s.empty?
|
|
28
|
+
File.realpath(found)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The runtime headers and archive ship beside the compiler: <root>/bin/spinel, <root>/lib.
|
|
33
|
+
def runtime_dir
|
|
34
|
+
@runtime_dir ||= begin
|
|
35
|
+
env = ENV["SPINEL_HDR_DIR"].to_s
|
|
36
|
+
candidates = [env, File.expand_path("../lib", File.dirname(spinel_bin)),
|
|
37
|
+
File.expand_path("lib", File.dirname(spinel_bin))]
|
|
38
|
+
candidates.find { |d| !d.empty? && File.exist?(File.join(d, "spinel_rt.h")) } or
|
|
39
|
+
raise Error, "spinel runtime (spinel_rt.h) not found next to #{spinel_bin}; set SPINEL_HDR_DIR"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def cache_dir
|
|
44
|
+
ENV["SPINEL_NATIVE_CACHE"] || File.join(ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache"), "spinel-native")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def which(cmd)
|
|
48
|
+
ENV["PATH"].split(File::PATH_SEPARATOR).each do |d|
|
|
49
|
+
p = File.join(d, cmd)
|
|
50
|
+
return p if File.executable?(p) && !File.directory?(p)
|
|
51
|
+
end
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def fingerprint
|
|
56
|
+
@fingerprint ||= begin
|
|
57
|
+
st = File.stat(spinel_bin)
|
|
58
|
+
Digest::SHA256.hexdigest([st.size, st.mtime.to_i, RUBY_VERSION, RUBY_PLATFORM, RbConfig::CONFIG["CC"]].join("|"))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def cc
|
|
63
|
+
Shellwords.split(RbConfig::CONFIG["CC"] || "cc")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The runtime archive Spinel ships is built for executables, not
|
|
67
|
+
# position-independent code, so a shared object cannot link it on
|
|
68
|
+
# Linux. Compile the runtime sources once per Spinel build into a
|
|
69
|
+
# -fPIC archive in the cache; the sources sit beside the headers.
|
|
70
|
+
def runtime_archive
|
|
71
|
+
@runtime_archive ||= begin
|
|
72
|
+
dir = File.join(cache_dir, "runtime_#{fingerprint[0, 12]}")
|
|
73
|
+
archive = File.join(dir, "libspinel_rt_pic.a")
|
|
74
|
+
build_runtime_archive(dir, archive) unless File.exist?(archive)
|
|
75
|
+
archive
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_runtime_archive(dir, archive)
|
|
80
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
81
|
+
FileUtils.mkdir_p(dir)
|
|
82
|
+
sources = Dir[File.join(runtime_dir, "*.c")] + Dir[File.join(runtime_dir, "regexp", "*.c")]
|
|
83
|
+
raise Error, "no runtime sources in #{runtime_dir}" if sources.empty?
|
|
84
|
+
jobs = Etc.respond_to?(:nprocessors) ? Etc.nprocessors : 4
|
|
85
|
+
queue = Queue.new
|
|
86
|
+
sources.each { |src| queue << src }
|
|
87
|
+
failures = Queue.new
|
|
88
|
+
objects = sources.map { |src| File.join(dir, File.basename(src, ".c") + ".o") }
|
|
89
|
+
Array.new(jobs) do
|
|
90
|
+
Thread.new do
|
|
91
|
+
while (src = queue.pop(true) rescue nil)
|
|
92
|
+
obj = File.join(dir, File.basename(src, ".c") + ".o")
|
|
93
|
+
cmd = [*cc, "-c", "-fPIC", "-O2", "-w", "-ffunction-sections", "-fdata-sections",
|
|
94
|
+
"-I#{runtime_dir}", "-I#{File.join(runtime_dir, 'regexp')}", src, "-o", obj]
|
|
95
|
+
out, status = Open3.capture2e(*cmd)
|
|
96
|
+
failures << "#{cmd.join(' ')}\n#{out}" unless status.success?
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end.each(&:join)
|
|
100
|
+
raise CompileError, "compiling the Spinel runtime failed:\n#{failures.pop}" unless failures.empty?
|
|
101
|
+
out, status = Open3.capture2e("ar", "rcs", archive, *objects)
|
|
102
|
+
raise CompileError, "ar failed:\n#{out}" unless status.success?
|
|
103
|
+
FileUtils.rm_f(objects)
|
|
104
|
+
Native.log("runtime compiled with -fPIC in #{(Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round(2)}s (#{archive})")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# +body+ is the module body (the `def self.` methods), +entries+ maps the
|
|
109
|
+
# exported names to their parameter types.
|
|
110
|
+
def initialize(body, entries)
|
|
111
|
+
@body = body
|
|
112
|
+
@entries = entries
|
|
113
|
+
digest = Digest::SHA256.hexdigest([self.class.fingerprint, body, entries.inspect].join("\0"))[0, 12]
|
|
114
|
+
@module_name = "SpinelKernel#{digest}"
|
|
115
|
+
@feature = "spinel_kernel_#{digest}"
|
|
116
|
+
@dir = File.join(self.class.cache_dir, @feature)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
attr_reader :module_name, :feature, :dir
|
|
120
|
+
|
|
121
|
+
def kernel_source
|
|
122
|
+
witness = @entries.map do |name, types|
|
|
123
|
+
" #{@module_name}.#{name}(#{types.map { |t| Types.witness(t) }.join(', ')})"
|
|
124
|
+
end
|
|
125
|
+
<<~RUBY
|
|
126
|
+
module #{@module_name}
|
|
127
|
+
#{@body.gsub(/^/, " ")}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if __FILE__ == $0
|
|
131
|
+
#{witness.join("\n")}
|
|
132
|
+
end
|
|
133
|
+
RUBY
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def bundle
|
|
137
|
+
File.join(@dir, "#{@feature}.#{RbConfig::CONFIG['DLEXT']}")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build
|
|
141
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
142
|
+
cached = File.exist?(bundle)
|
|
143
|
+
unless cached
|
|
144
|
+
FileUtils.mkdir_p(@dir)
|
|
145
|
+
File.write(File.join(@dir, "kernel.rb"), kernel_source)
|
|
146
|
+
run_spinel
|
|
147
|
+
run_cc
|
|
148
|
+
end
|
|
149
|
+
require bundle
|
|
150
|
+
Result.new(@module_name, bundle, @dir, cached, Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def run_spinel
|
|
156
|
+
entry_list = @entries.keys.map { |n| "#{@module_name}.#{n}" }.join(",")
|
|
157
|
+
cmd = [self.class.spinel_bin, File.join(@dir, "kernel.rb"), "-c", "--no-line-map",
|
|
158
|
+
"--ext", "cruby", "--ext-init", "spx_init_#{@feature}", "--ext-entry", entry_list,
|
|
159
|
+
"-o", File.join(@dir, "#{@feature}.c")]
|
|
160
|
+
sh(cmd, "spinel")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Every kernel carries its own copy of the runtime and exports the same
|
|
164
|
+
# global symbols. CRuby dlopens extensions RTLD_GLOBAL, and an ELF
|
|
165
|
+
# shared object binds its calls to the first definition in the
|
|
166
|
+
# process, so a second kernel would raise through the first kernel's
|
|
167
|
+
# exception stack and die as "unhandled". Export only Init_* and bind
|
|
168
|
+
# everything else inside the object. (Mach-O two-level namespaces do
|
|
169
|
+
# this by default.)
|
|
170
|
+
def shared_flags
|
|
171
|
+
return %w[-bundle -Wl,-undefined,dynamic_lookup] if RUBY_PLATFORM.include?("darwin")
|
|
172
|
+
script = File.join(@dir, "exports.map")
|
|
173
|
+
File.write(script, "{ global: Init_*; local: *; };\n")
|
|
174
|
+
["-shared", "-Wl,-Bsymbolic", "-Wl,--version-script=#{script}"]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def run_cc
|
|
178
|
+
cmd = [*self.class.cc, *shared_flags, "-fPIC", "-O2", "-w",
|
|
179
|
+
"-I#{RbConfig::CONFIG['rubyhdrdir']}", "-I#{RbConfig::CONFIG['rubyarchhdrdir']}",
|
|
180
|
+
"-I#{self.class.runtime_dir}", "-I#{@dir}",
|
|
181
|
+
File.join(@dir, "#{@feature}.c"), File.join(@dir, "#{@feature}_ext.c"),
|
|
182
|
+
self.class.runtime_archive, "-lm", "-o", bundle]
|
|
183
|
+
sh(cmd, "cc")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def sh(cmd, what)
|
|
187
|
+
Native.log(cmd.join(" "))
|
|
188
|
+
out, status = Open3.capture2e(*cmd)
|
|
189
|
+
File.write(File.join(@dir, "#{what}.log"), out)
|
|
190
|
+
return if status.success?
|
|
191
|
+
FileUtils.rm_f(bundle)
|
|
192
|
+
raise CompileError, "#{what} failed (exit #{status.exitstatus}) building #{@dir}:\n#{out}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spinel
|
|
4
|
+
module Native
|
|
5
|
+
# One per module that `extend Spinel::Native`. Tracks the methods marked
|
|
6
|
+
# `native`, keeps their pure-Ruby definitions, and swaps the compiled
|
|
7
|
+
# entries in once their parameter types are known.
|
|
8
|
+
class Registry
|
|
9
|
+
Entry = Struct.new(:name, :kind, :pure, :source, :types, :compiled, :module_function, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
attr_accessor :pending_signature
|
|
12
|
+
attr_reader :entries
|
|
13
|
+
|
|
14
|
+
def initialize(owner)
|
|
15
|
+
@owner = owner
|
|
16
|
+
@entries = {}
|
|
17
|
+
@last_def = nil
|
|
18
|
+
@installing = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Called from the method_added hooks.
|
|
22
|
+
def defined(kind, name)
|
|
23
|
+
return if @installing
|
|
24
|
+
@last_def = [kind, name]
|
|
25
|
+
return unless @pending_signature
|
|
26
|
+
sig = @pending_signature
|
|
27
|
+
@pending_signature = nil
|
|
28
|
+
mark(name, sig)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `native def name` (types nil: sampled from the first call) or a
|
|
32
|
+
# signature-declared entry.
|
|
33
|
+
def mark(name, types = nil)
|
|
34
|
+
kind = @last_def && @last_def[1] == name ? @last_def[0] : :instance
|
|
35
|
+
pure = kind == :singleton ? @owner.method(name) : @owner.instance_method(name)
|
|
36
|
+
entry = Entry.new(name: name, kind: kind, pure: pure, source: Source.of_method(pure), types: types,
|
|
37
|
+
# in a module, `native def x` is also callable as Mod.x (it never uses self)
|
|
38
|
+
module_function: kind == :instance && @owner.instance_of?(Module))
|
|
39
|
+
if types && pure.arity != types.length
|
|
40
|
+
raise TypeError, "#{name}: signature has #{types.length} parameter(s), the method takes #{pure.arity}"
|
|
41
|
+
end
|
|
42
|
+
@entries[name] = entry
|
|
43
|
+
install(entry) { |args, this| dispatch(entry, args, this) }
|
|
44
|
+
entry
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def compile_known!
|
|
48
|
+
known = @entries.values.select { |e| e.types && !e.compiled }
|
|
49
|
+
compile(known) unless known.empty?
|
|
50
|
+
known
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The pure definition, bound like the original call.
|
|
54
|
+
def pure_call(entry, args, this)
|
|
55
|
+
entry.kind == :singleton ? entry.pure.call(*args) : entry.pure.bind_call(this, *args)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# First call of a not-yet-compiled entry: settle its types, compile the
|
|
61
|
+
# kernel, then either forward or (verify mode) keep comparing.
|
|
62
|
+
def dispatch(entry, args, this)
|
|
63
|
+
return pure_call(entry, args, this) if Native.mode == :off
|
|
64
|
+
unless entry.compiled
|
|
65
|
+
entry.types ||= args.map { |a| Types.of_value(a) }
|
|
66
|
+
compile(@entries.values.select { |e| e.types && !e.compiled })
|
|
67
|
+
end
|
|
68
|
+
return pure_call(entry, args, this) unless entry.compiled
|
|
69
|
+
native_call(entry, args, this)
|
|
70
|
+
rescue Native::TypeError, CompileError => e
|
|
71
|
+
raise if Native.mode == :strict
|
|
72
|
+
warn "[spinel-native] #{@owner}##{entry.name}: staying on Ruby (#{e.message.lines.first.strip})"
|
|
73
|
+
entry.types = nil
|
|
74
|
+
install(entry) { |a, t| pure_call(entry, a, t) }
|
|
75
|
+
pure_call(entry, args, this)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def native_call(entry, args, this)
|
|
79
|
+
got = entry.compiled.public_send(entry.name, *args)
|
|
80
|
+
if Native.mode == :verify
|
|
81
|
+
want = pure_call(entry, args, this)
|
|
82
|
+
unless want == got || (want.is_a?(Float) && got.is_a?(Float) && want.nan? && got.nan?)
|
|
83
|
+
raise Mismatch, "#{@owner}##{entry.name}(#{args.map(&:inspect).join(', ')}): ruby=#{want.inspect} native=#{got.inspect}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
got
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Every marked method goes into the kernel (they may call each other);
|
|
90
|
+
# the ones with known types are exported.
|
|
91
|
+
def compile(exports)
|
|
92
|
+
body = @entries.values.map(&:source).join("\n\n")
|
|
93
|
+
builder = Builder.new(body, exports.to_h { |e| [e.name, e.types] })
|
|
94
|
+
result = builder.build
|
|
95
|
+
mod = Object.const_get(result.module_name)
|
|
96
|
+
exports.each do |e|
|
|
97
|
+
e.compiled = mod
|
|
98
|
+
if Native.mode == :verify
|
|
99
|
+
install(e) { |args, this| native_call(e, args, this) }
|
|
100
|
+
else
|
|
101
|
+
install(e) { |args, _this| mod.public_send(e.name, *args) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
Native.log("#{@owner}: #{exports.map { |e| "#{e.name}(#{e.types.map { |t| Types.to_s(t) }.join(', ')})" }.join(', ')} " \
|
|
105
|
+
"#{result.cached ? 'loaded from cache' : 'compiled'} in #{result.seconds.round(2)}s (#{result.dir})")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def install(entry, &body)
|
|
109
|
+
@installing = true
|
|
110
|
+
target = entry.kind == :singleton ? @owner.singleton_class : @owner
|
|
111
|
+
redefine(target, entry.name) { |*args| body.call(args, self) }
|
|
112
|
+
redefine(@owner.singleton_class, entry.name) { |*args| body.call(args, self) } if entry.module_function
|
|
113
|
+
ensure
|
|
114
|
+
@installing = false
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# define_method over an existing definition warns under -w; drop it first.
|
|
118
|
+
def redefine(target, name, &impl)
|
|
119
|
+
target.send(:remove_method, name) if target.method_defined?(name, false) || target.private_method_defined?(name, false)
|
|
120
|
+
target.send(:define_method, name, &impl)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spinel
|
|
4
|
+
module Native
|
|
5
|
+
# Pulls the source text of a method back out of its file with Prism, and
|
|
6
|
+
# rewrites its header to the `def self.name` form the kernel module uses.
|
|
7
|
+
module Source
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def of_method(meth)
|
|
11
|
+
file, line = meth.source_location
|
|
12
|
+
raise Error, "#{meth.name}: no source location (defined in C or eval?)" unless file && File.exist?(file)
|
|
13
|
+
node = find_def(Prism.parse_file(file).value, meth.name, line)
|
|
14
|
+
raise Error, "#{meth.name}: no `def` found at #{file}:#{line}" unless node
|
|
15
|
+
as_module_function(node)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def find_def(node, name, line)
|
|
19
|
+
return node if node.is_a?(Prism::DefNode) && node.name == name && node.location.start_line == line
|
|
20
|
+
node.compact_child_nodes.each do |child|
|
|
21
|
+
found = find_def(child, name, line)
|
|
22
|
+
return found if found
|
|
23
|
+
end
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def as_module_function(node)
|
|
28
|
+
raise Error, "#{node.name}: block parameters cannot cross the boundary" if node.parameters&.block
|
|
29
|
+
text = node.slice
|
|
30
|
+
text.sub(/\Adef\s+(self\s*\.\s*)?#{Regexp.escape(node.name.to_s)}/, "def self.#{node.name}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spinel
|
|
4
|
+
module Native
|
|
5
|
+
# The types that cross the extension boundary (by copy), and how a Ruby
|
|
6
|
+
# value maps onto them. A type is one of:
|
|
7
|
+
# :int, :float, :str, :bool, [:array, :int|:float|:str]
|
|
8
|
+
module Types
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
NAMES = {
|
|
12
|
+
"Integer" => :int, "Float" => :float, "String" => :str,
|
|
13
|
+
"bool" => :bool, "true" => :bool, "false" => :bool,
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# "(Array[Float], Integer) -> Float" => [[:array, :float], :int]
|
|
17
|
+
# The return type is inferred by Spinel and only checked for shape here.
|
|
18
|
+
def parse_signature(sig)
|
|
19
|
+
m = sig.strip.match(/\A\((.*)\)\s*->\s*(.+)\z/m)
|
|
20
|
+
raise TypeError, "bad signature #{sig.inspect}, want \"(T1, T2) -> R\"" unless m
|
|
21
|
+
params = split_top_level(m[1]).map { |t| parse_type(t) }
|
|
22
|
+
parse_type(m[2])
|
|
23
|
+
params
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse_type(text)
|
|
27
|
+
t = text.strip
|
|
28
|
+
if (m = t.match(/\AArray\[(.+)\]\z/))
|
|
29
|
+
elem = parse_type(m[1])
|
|
30
|
+
raise TypeError, "nested arrays cannot cross the boundary: #{t}" if elem.is_a?(Array)
|
|
31
|
+
return [:array, elem]
|
|
32
|
+
end
|
|
33
|
+
NAMES.fetch(t) { raise TypeError, "unsupported type #{t.inspect} (supported: Integer, Float, String, bool, Array[...])" }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def split_top_level(text)
|
|
37
|
+
out, depth, cur = [], 0, +""
|
|
38
|
+
text.each_char do |ch|
|
|
39
|
+
case ch
|
|
40
|
+
when "[" then depth += 1; cur << ch
|
|
41
|
+
when "]" then depth -= 1; cur << ch
|
|
42
|
+
when "," then depth.zero? ? (out << cur; cur = +"") : cur << ch
|
|
43
|
+
else cur << ch
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
out << cur unless cur.strip.empty?
|
|
47
|
+
out.map(&:strip)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The boundary type of a live Ruby value, or raise TypeError.
|
|
51
|
+
def of_value(v)
|
|
52
|
+
case v
|
|
53
|
+
when Integer then :int
|
|
54
|
+
when Float then :float
|
|
55
|
+
when String then :str
|
|
56
|
+
when true, false then :bool
|
|
57
|
+
when Array
|
|
58
|
+
raise TypeError, "cannot infer the element type of an empty array (declare a signature)" if v.empty?
|
|
59
|
+
elem = of_value(v.first)
|
|
60
|
+
raise TypeError, "nested arrays cannot cross the boundary" if elem.is_a?(Array)
|
|
61
|
+
unless v.all? { |e| of_value(e) == elem }
|
|
62
|
+
raise TypeError, "mixed-type array cannot cross the boundary"
|
|
63
|
+
end
|
|
64
|
+
[:array, elem]
|
|
65
|
+
else
|
|
66
|
+
raise TypeError, "#{v.class} cannot cross the boundary (Integer, Float, String, bool, Array of those)"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A Ruby literal of the type, used as the witness call that seeds
|
|
71
|
+
# Spinel's whole-program inference for an exported entry.
|
|
72
|
+
def witness(type)
|
|
73
|
+
case type
|
|
74
|
+
in :int then "0"
|
|
75
|
+
in :float then "0.0"
|
|
76
|
+
in :str then '"x"'
|
|
77
|
+
in :bool then "true"
|
|
78
|
+
in [:array, elem] then "[#{witness(elem)}]"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def to_s(type)
|
|
83
|
+
case type
|
|
84
|
+
in :int then "Integer"
|
|
85
|
+
in :float then "Float"
|
|
86
|
+
in :str then "String"
|
|
87
|
+
in :bool then "bool"
|
|
88
|
+
in [:array, elem] then "Array[#{to_s(elem)}]"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Spinel::Native -- compile individual Ruby methods to a native extension with
|
|
4
|
+
# the Spinel AOT compiler, from inside a running CRuby program.
|
|
5
|
+
#
|
|
6
|
+
# module Physics
|
|
7
|
+
# extend Spinel::Native
|
|
8
|
+
#
|
|
9
|
+
# native def dot(a, b)
|
|
10
|
+
# s = 0.0
|
|
11
|
+
# i = 0
|
|
12
|
+
# while i < a.length
|
|
13
|
+
# s += a[i] * b[i]
|
|
14
|
+
# i += 1
|
|
15
|
+
# end
|
|
16
|
+
# s
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# The method keeps running as plain Ruby until it is first called; the
|
|
21
|
+
# argument types of that first call seed Spinel's type inference, the kernel
|
|
22
|
+
# is compiled into a CRuby extension (cached on disk), and the method is
|
|
23
|
+
# re-bound to the compiled entry. If anything fails the pure-Ruby definition
|
|
24
|
+
# stays in place, so the program is never worse off than without the gem.
|
|
25
|
+
#
|
|
26
|
+
# SPINEL_NATIVE=off never compile, run the Ruby definitions
|
|
27
|
+
# SPINEL_NATIVE=verify run both and raise Spinel::Native::Mismatch on divergence
|
|
28
|
+
# SPINEL_NATIVE=strict a compile failure raises instead of falling back
|
|
29
|
+
|
|
30
|
+
require "rbconfig"
|
|
31
|
+
require "digest"
|
|
32
|
+
require "fileutils"
|
|
33
|
+
require "prism"
|
|
34
|
+
|
|
35
|
+
require_relative "native/version"
|
|
36
|
+
require_relative "native/types"
|
|
37
|
+
require_relative "native/source"
|
|
38
|
+
require_relative "native/builder"
|
|
39
|
+
require_relative "native/registry"
|
|
40
|
+
|
|
41
|
+
module Spinel
|
|
42
|
+
module Native
|
|
43
|
+
class Error < StandardError; end
|
|
44
|
+
class CompileError < Error; end
|
|
45
|
+
class TypeError < Error; end
|
|
46
|
+
class Mismatch < Error; end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
# :on (default), :off, :verify, :strict
|
|
50
|
+
def mode
|
|
51
|
+
@mode ||= (ENV["SPINEL_NATIVE"] || "on").to_sym
|
|
52
|
+
end
|
|
53
|
+
attr_writer :mode
|
|
54
|
+
|
|
55
|
+
def verbose?
|
|
56
|
+
ENV["SPINEL_NATIVE_VERBOSE"] == "1"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def log(msg)
|
|
60
|
+
warn("[spinel-native] #{msg}") if verbose?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Compile every native method of +mod+ whose parameter types are known
|
|
64
|
+
# (from a signature or an earlier call). Useful at boot to pay the
|
|
65
|
+
# compile cost up front and surface errors early.
|
|
66
|
+
def compile!(mod)
|
|
67
|
+
registry_of(mod).compile_known!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The registry behind a module that has `extend Spinel::Native`.
|
|
71
|
+
def registry_of(mod)
|
|
72
|
+
mod.instance_variable_get(:@__spinel_native) or
|
|
73
|
+
raise Error, "#{mod} does not extend Spinel::Native"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def extended(base)
|
|
77
|
+
base.instance_variable_set(:@__spinel_native, Registry.new(base))
|
|
78
|
+
base.singleton_class.prepend(Hooks)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Records which `def` ran last so `native def x` knows what it marked.
|
|
83
|
+
module Hooks
|
|
84
|
+
def method_added(name)
|
|
85
|
+
@__spinel_native&.defined(:instance, name)
|
|
86
|
+
super
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def singleton_method_added(name)
|
|
90
|
+
@__spinel_native&.defined(:singleton, name)
|
|
91
|
+
super
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# native def foo(a, b) ... end types sampled from the first call
|
|
96
|
+
# native "(Array[Float], Integer) -> Float"; def foo(a, b) ... end
|
|
97
|
+
# types declared, compiled on first call
|
|
98
|
+
def native(arg = nil)
|
|
99
|
+
registry = @__spinel_native
|
|
100
|
+
case arg
|
|
101
|
+
when Symbol then registry.mark(arg)
|
|
102
|
+
when String then registry.pending_signature = Types.parse_signature(arg)
|
|
103
|
+
when nil then raise ArgumentError, "native: expected `native def ...` or `native \"(...) -> ...\"`"
|
|
104
|
+
else raise ArgumentError, "native: unexpected #{arg.inspect}"
|
|
105
|
+
end
|
|
106
|
+
arg
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: spinel_native
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Chris Hasiński
|
|
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: prism
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Mark a hot method with `native def`; on its first call it is compiled
|
|
27
|
+
by Spinel into a CRuby extension and rebound, with the Ruby definition kept as fallback
|
|
28
|
+
and oracle.
|
|
29
|
+
email:
|
|
30
|
+
- krzysztof.hasinski@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- LICENSE
|
|
37
|
+
- README.md
|
|
38
|
+
- examples/demo.rb
|
|
39
|
+
- lib/spinel/native.rb
|
|
40
|
+
- lib/spinel/native/builder.rb
|
|
41
|
+
- lib/spinel/native/registry.rb
|
|
42
|
+
- lib/spinel/native/source.rb
|
|
43
|
+
- lib/spinel/native/types.rb
|
|
44
|
+
- lib/spinel/native/version.rb
|
|
45
|
+
homepage: https://github.com/khasinski/spinel_native
|
|
46
|
+
licenses:
|
|
47
|
+
- MIT
|
|
48
|
+
metadata:
|
|
49
|
+
source_code_uri: https://github.com/khasinski/spinel_native
|
|
50
|
+
changelog_uri: https://github.com/khasinski/spinel_native/blob/main/CHANGELOG.md
|
|
51
|
+
bug_tracker_uri: https://github.com/khasinski/spinel_native/issues
|
|
52
|
+
rubygems_mfa_required: 'true'
|
|
53
|
+
rdoc_options: []
|
|
54
|
+
require_paths:
|
|
55
|
+
- lib
|
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.4'
|
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0'
|
|
66
|
+
requirements: []
|
|
67
|
+
rubygems_version: 4.0.16
|
|
68
|
+
specification_version: 4
|
|
69
|
+
summary: Compile individual Ruby methods to native code with the Spinel AOT compiler
|
|
70
|
+
test_files: []
|