spinel_native 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +22 -1
- data/README.md +266 -21
- data/lib/spinel/native/builder.rb +3 -3
- data/lib/spinel/native/registry.rb +85 -4
- data/lib/spinel/native/source.rb +22 -0
- data/lib/spinel/native/version.rb +1 -1
- data/lib/spinel/native.rb +52 -0
- 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: 861579157d9270857eede60bc891c64f7b6b78c152a62512ef0ad098f112fad4
|
|
4
|
+
data.tar.gz: 132e7349789d035475fd5386ffa5bf6c60b39a3c08880e9adcd01244563d2e4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b195d6cf158a5bc71c802c7f181f9956a868c1989fd3901c6cd2ea93bfd04ddcb40eaf08fcd92e34e09f10253a4403ce2ef4275aa2d084332d6c75a7318ea1f2
|
|
7
|
+
data.tar.gz: 2ed20f171858b8f635bfda4dc66cbad803235430bd162a3cdbc32b3b8dc741acb4638f618c0b361b5eb4d3789b8a3dd094354df457ee6546fc14efe8810edc90
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.3.0 (2026-09-18)
|
|
4
|
+
|
|
5
|
+
- `native_entries :a, :b`: in a stateful kernel, name the methods called from
|
|
6
|
+
Ruby (exported across the extension boundary). Every other native method
|
|
7
|
+
stays internal to the kernel, so its parameters and return value need not be
|
|
8
|
+
boundary types and it needs no signature -- letting a real stateful renderer
|
|
9
|
+
keep poly-typed helpers behind a small typed surface. A non-entry method
|
|
10
|
+
called from Ruby runs its Ruby definition on the module's Ruby-side ivars,
|
|
11
|
+
not the kernel state; treat it as private to the kernel.
|
|
12
|
+
|
|
13
|
+
## 0.2.0 (2026-09-16)
|
|
14
|
+
|
|
15
|
+
- `native_state { ... }`: module state kept inside the compiled kernel
|
|
16
|
+
across calls. A stateful module is compiled as a whole when its body ends
|
|
17
|
+
and needs a signature on every native method; the block also initialises
|
|
18
|
+
the Ruby definitions' ivars.
|
|
19
|
+
- `native_prelude`: constants, Structs and helper defs emitted into the
|
|
20
|
+
kernel ahead of the native methods.
|
|
21
|
+
|
|
22
|
+
## 0.1.0 (2026-09-15)
|
|
23
|
+
|
|
24
|
+
First release on RubyGems.
|
|
4
25
|
|
|
5
26
|
- `native def` marks a method; its first call samples the argument types,
|
|
6
27
|
compiles the kernel with `spinel --ext cruby`, and rebinds the method.
|
data/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# spinel_native
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/spinel_native)
|
|
4
|
+
[](https://github.com/khasinski/spinel_native/actions/workflows/ci.yml)
|
|
5
|
+
|
|
3
6
|
Compile a single Ruby method to native code with the
|
|
4
7
|
[Spinel](https://github.com/matz/spinel) AOT compiler, from inside a running
|
|
5
8
|
CRuby program. Mark the hot method, keep everything else on CRuby.
|
|
@@ -33,34 +36,259 @@ stays in place and a warning says why.
|
|
|
33
36
|
|
|
34
37
|
## Interface
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
### Mark a method
|
|
40
|
+
|
|
41
|
+
`native def` is the whole DSL. Types are sampled from the first call.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
module Physics
|
|
45
|
+
extend Spinel::Native
|
|
46
|
+
|
|
47
|
+
native def dot(a, b)
|
|
48
|
+
s = 0.0
|
|
49
|
+
i = 0
|
|
50
|
+
while i < a.length
|
|
51
|
+
s += a[i] * b[i]
|
|
52
|
+
i += 1
|
|
53
|
+
end
|
|
54
|
+
s
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Physics.dot([1.0, 2.0], [3.0, 4.0]) # => 11.0, compiled on this call
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
In a module the method is also callable as `Physics.dot`, since a native
|
|
62
|
+
method never uses `self`. Singleton and instance forms work the same way:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
module Text
|
|
66
|
+
extend Spinel::Native
|
|
67
|
+
|
|
68
|
+
native def self.count_vowels(s)
|
|
69
|
+
n = 0
|
|
70
|
+
s.each_char { |c| n += 1 if "aeiou".include?(c) }
|
|
71
|
+
n
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class Sieve
|
|
76
|
+
extend Spinel::Native
|
|
77
|
+
|
|
78
|
+
native def count_primes(n)
|
|
79
|
+
flags = Array.new(n + 1, true)
|
|
80
|
+
count = 0
|
|
81
|
+
i = 2
|
|
82
|
+
while i <= n
|
|
83
|
+
if flags[i]
|
|
84
|
+
count += 1
|
|
85
|
+
j = i * i
|
|
86
|
+
while j <= n
|
|
87
|
+
flags[j] = false
|
|
88
|
+
j += i
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
i += 1
|
|
92
|
+
end
|
|
93
|
+
count
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
Text.count_vowels("spinel") # => 2
|
|
98
|
+
Sieve.new.count_primes(100) # => 25
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Native methods may call each other; everything reachable must be `native`
|
|
102
|
+
too, because the kernel is exactly the set of marked methods:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
module Fractal
|
|
106
|
+
extend Spinel::Native
|
|
107
|
+
|
|
108
|
+
native def mandel(cr, ci, limit)
|
|
109
|
+
zr = zi = 0.0
|
|
110
|
+
n = 0
|
|
111
|
+
while n < limit && zr * zr + zi * zi < 4.0
|
|
112
|
+
zr, zi = zr * zr - zi * zi + cr, 2.0 * zr * zi + ci
|
|
113
|
+
n += 1
|
|
114
|
+
end
|
|
115
|
+
n
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
native def row(ci, width, limit)
|
|
119
|
+
(0...width).map { |x| mandel(-2.0 + 3.0 * x / width, ci, limit) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Declare the types
|
|
125
|
+
|
|
126
|
+
A signature on the line before the `def` replaces sampling. The return
|
|
127
|
+
type is inferred by Spinel and only checked for shape.
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
module Stats
|
|
131
|
+
extend Spinel::Native
|
|
132
|
+
|
|
133
|
+
native "(Array[Float]) -> Float"
|
|
134
|
+
def mean(xs)
|
|
135
|
+
xs.sum / xs.length
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Declared entries can be compiled at boot, so a compile error surfaces
|
|
141
|
+
before the first request rather than in the middle of one:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
Spinel::Native.compile!(Stats)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Keep state between calls
|
|
148
|
+
|
|
149
|
+
`native_state` gives the module state that lives inside the compiled kernel,
|
|
150
|
+
so a large input can be copied in once and queried many times:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
module Index
|
|
154
|
+
extend Spinel::Native
|
|
155
|
+
|
|
156
|
+
native_state do
|
|
157
|
+
@docs = []
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
native "(Array[String]) -> Integer"
|
|
161
|
+
def load(docs)
|
|
162
|
+
@docs = docs
|
|
163
|
+
docs.length
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
native "(String) -> Array[Integer]"
|
|
167
|
+
def find(word)
|
|
168
|
+
hits = []
|
|
169
|
+
i = 0
|
|
170
|
+
while i < @docs.length
|
|
171
|
+
hits << i if @docs[i].include?(word)
|
|
172
|
+
i += 1
|
|
173
|
+
end
|
|
174
|
+
hits
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
Index.load(corpus) # copied across once
|
|
179
|
+
Index.find("spinel") # no copy, just the answer
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
A stateful module is one kernel with one state, so it is compiled as a
|
|
183
|
+
whole the moment its body ends; there is nothing to call first. Every native
|
|
184
|
+
method in it therefore needs a declared signature, and a missing one is
|
|
185
|
+
reported when the module closes. The block also runs on the module itself,
|
|
186
|
+
so the Ruby definitions (used by `off`, `verify`, and the fallback) start
|
|
187
|
+
from the same state. State is per process, not per object: a stateful
|
|
188
|
+
module is a singleton, and a forked worker gets its own copy.
|
|
189
|
+
|
|
190
|
+
### Extra kernel source
|
|
191
|
+
|
|
192
|
+
Only `native` methods are pulled out of the source file. Constants, `Struct`
|
|
193
|
+
definitions, and helpers the kernel needs but that are not entries go in a
|
|
194
|
+
`native_prelude`:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
module Raster
|
|
198
|
+
extend Spinel::Native
|
|
199
|
+
|
|
200
|
+
native_prelude <<~RUBY
|
|
201
|
+
WIDTH = 320
|
|
202
|
+
Point = Struct.new(:x, :y)
|
|
203
|
+
|
|
204
|
+
def self.clamp(v, lo, hi)
|
|
205
|
+
v < lo ? lo : (v > hi ? hi : v)
|
|
206
|
+
end
|
|
207
|
+
RUBY
|
|
208
|
+
|
|
209
|
+
native def pixel(x, y)
|
|
210
|
+
clamp(y, 0, 239) * WIDTH + clamp(x, 0, WIDTH - 1)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Choose the entry methods
|
|
216
|
+
|
|
217
|
+
A stateful module is compiled as one kernel, and by default every native method
|
|
218
|
+
is an entry that Ruby can call -- so every one needs a boundary-crossable
|
|
219
|
+
signature. `native_entries` names the few methods actually called from Ruby; the
|
|
220
|
+
rest stay internal to the kernel, reachable only from other native methods.
|
|
221
|
+
Internal methods need no signature and their parameters and return value need
|
|
222
|
+
not be boundary types (they may be poly and get boxed) -- which is what lets a
|
|
223
|
+
real, mutually-recursive kernel expose a small typed surface:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
module Renderer
|
|
227
|
+
extend Spinel::Native
|
|
228
|
+
|
|
229
|
+
native_state { @fb = Array.new(76800, 0) }
|
|
230
|
+
native_entries :render # the only method CRuby calls
|
|
231
|
+
|
|
232
|
+
native "(Integer, Integer, Float) -> Array[Integer]"
|
|
233
|
+
def render(px, py, angle)
|
|
234
|
+
draw_walls(px, py, angle) # internal; no signature required
|
|
235
|
+
@fb
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
native def draw_walls(px, py, angle) # stays inside the kernel
|
|
239
|
+
# ...
|
|
240
|
+
0
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Internal methods are still defined on the Ruby module, but calling one from
|
|
246
|
+
Ruby runs its **Ruby** definition, against the module's Ruby-side ivars, not
|
|
247
|
+
the kernel's state. `Renderer.draw_walls(...)` would draw into the Ruby
|
|
248
|
+
`@fb`, which the compiled `render` never sees. Treat internal methods as
|
|
249
|
+
private to the kernel; make a method an entry if Ruby needs its result.
|
|
250
|
+
|
|
251
|
+
### Modes
|
|
252
|
+
|
|
253
|
+
The Ruby definition is always kept. Which path runs is a process-wide
|
|
254
|
+
switch, settable from code or from the environment:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
Spinel::Native.mode = :on # default: compile on first call, fall back on failure
|
|
258
|
+
Spinel::Native.mode = :off # never compile, run the Ruby definitions
|
|
259
|
+
Spinel::Native.mode = :verify # run both, raise Spinel::Native::Mismatch if they differ
|
|
260
|
+
Spinel::Native.mode = :strict # a compile or type failure raises instead of falling back
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```sh
|
|
264
|
+
SPINEL_NATIVE=verify ruby app.rb # the Ruby definition is the oracle
|
|
265
|
+
SPINEL_NATIVE=off ruby app.rb # e.g. in a debugger
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Environment
|
|
269
|
+
|
|
270
|
+
```sh
|
|
271
|
+
SPINEL=/path/to/spinel/bin/spinel # the compiler; otherwise `spinel` on PATH
|
|
272
|
+
SPINEL_HDR_DIR=/path/to/lib # runtime headers, if not next to the binary
|
|
273
|
+
SPINEL_NATIVE_CACHE=~/.cache/spinel-native # where compiled kernels live
|
|
274
|
+
SPINEL_NATIVE_VERBOSE=1 # print the commands and timings
|
|
275
|
+
```
|
|
51
276
|
|
|
52
277
|
## Rules for a native method
|
|
53
278
|
|
|
54
279
|
- Parameters and the return value cross the boundary **by copy**. Supported
|
|
55
280
|
types: `Integer` (64-bit), `Float`, `String`, `bool`, and `Array` of those.
|
|
56
281
|
Mutating a parameter is refused at compile time; return the result instead.
|
|
57
|
-
- The body must not touch `self
|
|
58
|
-
|
|
59
|
-
`native` too, because the kernel is exactly the set of marked methods.
|
|
282
|
+
- The body must not touch `self` or anything outside the module's `native`
|
|
283
|
+
methods, its `native_prelude`, and its `native_state` ivars.
|
|
60
284
|
- A `raise` inside the kernel arrives in Ruby as the same exception class and
|
|
61
285
|
message. Integer overflow raises `RangeError` where CRuby would promote to a
|
|
62
286
|
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
|
|
287
|
+
- The kernel runs without the GVL, one call at a time per module, so
|
|
288
|
+
`native_state` needs no locking of its own.
|
|
289
|
+
- In a stateful module with `native_entries`, only the entries reach the
|
|
290
|
+
kernel's state. A non-entry method called from Ruby runs as Ruby, on the
|
|
291
|
+
module's own ivars.
|
|
64
292
|
|
|
65
293
|
## How it works
|
|
66
294
|
|
|
@@ -84,14 +312,31 @@ stays in place and a warning says why.
|
|
|
84
312
|
|
|
85
313
|
## Install
|
|
86
314
|
|
|
87
|
-
|
|
315
|
+
The gem is on RubyGems:
|
|
316
|
+
|
|
317
|
+
```sh
|
|
318
|
+
gem install spinel_native
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
or in a Gemfile:
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
gem "spinel_native"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The gem needs the Spinel compiler at run time, and Spinel is not on RubyGems.
|
|
328
|
+
Build it from source once and point the gem at the binary, either through
|
|
329
|
+
`SPINEL` or by putting `spinel` on `PATH`:
|
|
88
330
|
|
|
89
331
|
```sh
|
|
90
332
|
git clone https://github.com/matz/spinel && cd spinel && make deps && make
|
|
91
333
|
export SPINEL=$PWD/bin/spinel
|
|
92
|
-
gem install spinel_native # or: gem "spinel_native" in the Gemfile
|
|
93
334
|
```
|
|
94
335
|
|
|
336
|
+
The runtime headers and sources are found next to the binary. If Spinel
|
|
337
|
+
was installed elsewhere, `SPINEL_HDR_DIR` names the directory with
|
|
338
|
+
`spinel_rt.h`.
|
|
339
|
+
|
|
95
340
|
## Running the example
|
|
96
341
|
|
|
97
342
|
```sh
|
|
@@ -107,10 +107,10 @@ module Spinel
|
|
|
107
107
|
|
|
108
108
|
# +body+ is the module body (the `def self.` methods), +entries+ maps the
|
|
109
109
|
# exported names to their parameter types.
|
|
110
|
-
def initialize(body, entries)
|
|
111
|
-
@body = body
|
|
110
|
+
def initialize(body, entries, state: nil)
|
|
111
|
+
@body = state && !state.empty? ? "#{state}\n\n#{body}" : body
|
|
112
112
|
@entries = entries
|
|
113
|
-
digest = Digest::SHA256.hexdigest([self.class.fingerprint, body, entries.inspect].join("\0"))[0, 12]
|
|
113
|
+
digest = Digest::SHA256.hexdigest([self.class.fingerprint, @body, entries.inspect].join("\0"))[0, 12]
|
|
114
114
|
@module_name = "SpinelKernel#{digest}"
|
|
115
115
|
@feature = "spinel_kernel_#{digest}"
|
|
116
116
|
@dir = File.join(self.class.cache_dir, @feature)
|
|
@@ -9,13 +9,57 @@ module Spinel
|
|
|
9
9
|
Entry = Struct.new(:name, :kind, :pure, :source, :types, :compiled, :module_function, keyword_init: true)
|
|
10
10
|
|
|
11
11
|
attr_accessor :pending_signature
|
|
12
|
-
attr_reader :entries
|
|
12
|
+
attr_reader :entries, :prelude
|
|
13
13
|
|
|
14
14
|
def initialize(owner)
|
|
15
15
|
@owner = owner
|
|
16
16
|
@entries = {}
|
|
17
|
+
@prelude = []
|
|
17
18
|
@last_def = nil
|
|
18
19
|
@installing = false
|
|
20
|
+
@state = nil
|
|
21
|
+
@disabled = false
|
|
22
|
+
@entry_names = nil
|
|
23
|
+
@stateful_compiled = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def stateful?
|
|
27
|
+
!@state.nil?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Restrict which methods of a stateful kernel are exported across the
|
|
31
|
+
# extension boundary (called from Ruby). The rest stay internal to the
|
|
32
|
+
# kernel -- reachable only from other native methods -- so their parameter
|
|
33
|
+
# and return types need not be boundary-crossable (they may be poly and
|
|
34
|
+
# get boxed). Without this every native method is an entry.
|
|
35
|
+
def set_entries(names)
|
|
36
|
+
@entry_names = names.map(&:to_sym)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# `native_state { ... }`: run the block on the module so the Ruby
|
|
40
|
+
# definitions start from the same state, keep its source for the
|
|
41
|
+
# kernel, and arm a compile of the whole module for when its body ends.
|
|
42
|
+
def state(block)
|
|
43
|
+
raise Error, "#{@owner}: native_state declared twice" if @state
|
|
44
|
+
@state = Source.of_state_block(block)
|
|
45
|
+
@owner.instance_exec(&block)
|
|
46
|
+
owner = @owner
|
|
47
|
+
tp = TracePoint.new(:end) do |ev|
|
|
48
|
+
next unless ev.self.equal?(owner)
|
|
49
|
+
tp.disable
|
|
50
|
+
compile_stateful
|
|
51
|
+
end
|
|
52
|
+
@end_hook = tp
|
|
53
|
+
tp.enable
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Extra top-level source (constants, Structs, plain helper defs) emitted
|
|
57
|
+
# into the kernel module ahead of the marked methods. The kernel is
|
|
58
|
+
# otherwise exactly the set of `native` methods, so anything they
|
|
59
|
+
# reference by name -- a SCREEN_WIDTH constant, a state Struct, a helper
|
|
60
|
+
# that is not itself an exported entry -- has to be declared here.
|
|
61
|
+
def add_prelude(source)
|
|
62
|
+
@prelude << source.to_s
|
|
19
63
|
end
|
|
20
64
|
|
|
21
65
|
# Called from the method_added hooks.
|
|
@@ -45,6 +89,7 @@ module Spinel
|
|
|
45
89
|
end
|
|
46
90
|
|
|
47
91
|
def compile_known!
|
|
92
|
+
return compile_stateful ? @entries.values : [] if stateful?
|
|
48
93
|
known = @entries.values.select { |e| e.types && !e.compiled }
|
|
49
94
|
compile(known) unless known.empty?
|
|
50
95
|
known
|
|
@@ -60,7 +105,13 @@ module Spinel
|
|
|
60
105
|
# First call of a not-yet-compiled entry: settle its types, compile the
|
|
61
106
|
# kernel, then either forward or (verify mode) keep comparing.
|
|
62
107
|
def dispatch(entry, args, this)
|
|
63
|
-
return pure_call(entry, args, this) if Native.mode == :off
|
|
108
|
+
return pure_call(entry, args, this) if Native.mode == :off || @disabled
|
|
109
|
+
if stateful?
|
|
110
|
+
# A module without a closing `end` event (Module.new) compiles here instead.
|
|
111
|
+
compile_stateful unless entry.compiled
|
|
112
|
+
return pure_call(entry, args, this) unless entry.compiled
|
|
113
|
+
return native_call(entry, args, this)
|
|
114
|
+
end
|
|
64
115
|
unless entry.compiled
|
|
65
116
|
entry.types ||= args.map { |a| Types.of_value(a) }
|
|
66
117
|
compile(@entries.values.select { |e| e.types && !e.compiled })
|
|
@@ -75,6 +126,36 @@ module Spinel
|
|
|
75
126
|
pure_call(entry, args, this)
|
|
76
127
|
end
|
|
77
128
|
|
|
129
|
+
# A stateful module is one kernel with one state, so it is compiled in
|
|
130
|
+
# full, once: every native method must carry a declared signature. Runs
|
|
131
|
+
# when the module body ends, or at the first call if that never fires.
|
|
132
|
+
# Returns true when the module is now native.
|
|
133
|
+
def compile_stateful
|
|
134
|
+
@end_hook&.disable
|
|
135
|
+
return false if @disabled || Native.mode == :off
|
|
136
|
+
return true if @stateful_compiled
|
|
137
|
+
exports = @entry_names ? @entries.values.select { |e| @entry_names.include?(e.name) } : @entries.values
|
|
138
|
+
raise Native::TypeError, "#{@owner}: native_entries names no known method" if exports.empty?
|
|
139
|
+
# Only exported (boundary-crossing) methods need signatures; internal
|
|
140
|
+
# helpers are inferred from the call graph, like a non-stateful kernel.
|
|
141
|
+
untyped = exports.reject(&:types).map(&:name)
|
|
142
|
+
unless untyped.empty?
|
|
143
|
+
raise Native::TypeError, "#{@owner} keeps state, so every native entry needs a signature; missing: #{untyped.join(', ')}"
|
|
144
|
+
end
|
|
145
|
+
compile(exports)
|
|
146
|
+
# Internal helpers are compiled into the kernel but not callable from
|
|
147
|
+
# Ruby; if one is invoked directly, fall back to its Ruby definition.
|
|
148
|
+
(@entries.values - exports).each { |e| install(e) { |a, t| pure_call(e, a, t) } }
|
|
149
|
+
@stateful_compiled = true
|
|
150
|
+
true
|
|
151
|
+
rescue Native::TypeError, CompileError => e
|
|
152
|
+
raise if Native.mode == :strict
|
|
153
|
+
warn "[spinel-native] #{@owner}: staying on Ruby (#{e.message.lines.first.strip})"
|
|
154
|
+
@disabled = true
|
|
155
|
+
@entries.each_value { |entry| install(entry) { |a, t| pure_call(entry, a, t) } }
|
|
156
|
+
false
|
|
157
|
+
end
|
|
158
|
+
|
|
78
159
|
def native_call(entry, args, this)
|
|
79
160
|
got = entry.compiled.public_send(entry.name, *args)
|
|
80
161
|
if Native.mode == :verify
|
|
@@ -89,8 +170,8 @@ module Spinel
|
|
|
89
170
|
# Every marked method goes into the kernel (they may call each other);
|
|
90
171
|
# the ones with known types are exported.
|
|
91
172
|
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] })
|
|
173
|
+
body = (@prelude + @entries.values.map(&:source)).join("\n\n")
|
|
174
|
+
builder = Builder.new(body, exports.to_h { |e| [e.name, e.types] }, state: @state)
|
|
94
175
|
result = builder.build
|
|
95
176
|
mod = Object.const_get(result.module_name)
|
|
96
177
|
exports.each do |e|
|
data/lib/spinel/native/source.rb
CHANGED
|
@@ -15,6 +15,28 @@ module Spinel
|
|
|
15
15
|
as_module_function(node)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# The body of the `native_state { ... }` block at the block's source location.
|
|
19
|
+
def of_state_block(block)
|
|
20
|
+
file, line = block.source_location
|
|
21
|
+
raise Error, "native_state: no source location" unless file && File.exist?(file)
|
|
22
|
+
call = find_state_call(Prism.parse_file(file).value, line)
|
|
23
|
+
raise Error, "native_state: no `native_state { ... }` found at #{file}:#{line}" unless call
|
|
24
|
+
body = call.block.body
|
|
25
|
+
body ? body.slice : ""
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def find_state_call(node, line)
|
|
29
|
+
if node.is_a?(Prism::CallNode) && node.name == :native_state &&
|
|
30
|
+
node.block.is_a?(Prism::BlockNode) && node.location.start_line == line
|
|
31
|
+
return node
|
|
32
|
+
end
|
|
33
|
+
node.compact_child_nodes.each do |child|
|
|
34
|
+
found = find_state_call(child, line)
|
|
35
|
+
return found if found
|
|
36
|
+
end
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
18
40
|
def find_def(node, name, line)
|
|
19
41
|
return node if node.is_a?(Prism::DefNode) && node.name == name && node.location.start_line == line
|
|
20
42
|
node.compact_child_nodes.each do |child|
|
data/lib/spinel/native.rb
CHANGED
|
@@ -23,6 +23,21 @@
|
|
|
23
23
|
# re-bound to the compiled entry. If anything fails the pure-Ruby definition
|
|
24
24
|
# stays in place, so the program is never worse off than without the gem.
|
|
25
25
|
#
|
|
26
|
+
# A module can also keep state inside the kernel between calls:
|
|
27
|
+
#
|
|
28
|
+
# module Index
|
|
29
|
+
# extend Spinel::Native
|
|
30
|
+
# native_state { @docs = [] }
|
|
31
|
+
# native "(Array[String]) -> Integer"
|
|
32
|
+
# def load(docs) = (@docs = docs).length
|
|
33
|
+
# native "(Integer) -> String"
|
|
34
|
+
# def doc(i) = @docs[i]
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# Such a module is compiled as a whole when its body ends (all its native
|
|
38
|
+
# methods must declare signatures), and the Ruby definitions run against the
|
|
39
|
+
# module's own ivars, initialised by the same block.
|
|
40
|
+
#
|
|
26
41
|
# SPINEL_NATIVE=off never compile, run the Ruby definitions
|
|
27
42
|
# SPINEL_NATIVE=verify run both and raise Spinel::Native::Mismatch on divergence
|
|
28
43
|
# SPINEL_NATIVE=strict a compile failure raises instead of falling back
|
|
@@ -92,6 +107,17 @@ module Spinel
|
|
|
92
107
|
end
|
|
93
108
|
end
|
|
94
109
|
|
|
110
|
+
# native_state { @items = []; @sum = 0 }
|
|
111
|
+
#
|
|
112
|
+
# Module-level state that lives inside the compiled kernel across calls
|
|
113
|
+
# (and, for the Ruby fallback, in the module's own ivars). A stateful
|
|
114
|
+
# module is compiled as a whole when its body ends, so every native
|
|
115
|
+
# method in it needs a declared signature.
|
|
116
|
+
def native_state(&block)
|
|
117
|
+
raise ArgumentError, "native_state needs a block" unless block
|
|
118
|
+
@__spinel_native.state(block)
|
|
119
|
+
end
|
|
120
|
+
|
|
95
121
|
# native def foo(a, b) ... end types sampled from the first call
|
|
96
122
|
# native "(Array[Float], Integer) -> Float"; def foo(a, b) ... end
|
|
97
123
|
# types declared, compiled on first call
|
|
@@ -105,5 +131,31 @@ module Spinel
|
|
|
105
131
|
end
|
|
106
132
|
arg
|
|
107
133
|
end
|
|
134
|
+
|
|
135
|
+
# Emit extra top-level source into the compiled kernel, ahead of the marked
|
|
136
|
+
# methods: constants, `Struct.new` state aggregates, and plain helper defs
|
|
137
|
+
# the native methods call. Only `native` methods are pulled from their
|
|
138
|
+
# source files; everything else a kernel needs must be declared here.
|
|
139
|
+
#
|
|
140
|
+
# native_prelude <<~RUBY
|
|
141
|
+
# WIDTH = 320
|
|
142
|
+
# St = Struct.new(:buf, :n)
|
|
143
|
+
# RUBY
|
|
144
|
+
def native_prelude(source)
|
|
145
|
+
@__spinel_native.add_prelude(source)
|
|
146
|
+
source
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# native_entries :render, :load_map
|
|
150
|
+
#
|
|
151
|
+
# In a stateful kernel, name the methods that are called from Ruby (exported
|
|
152
|
+
# across the extension boundary). Every other native method stays internal
|
|
153
|
+
# to the kernel -- reachable only from native code -- so its parameter and
|
|
154
|
+
# return types need not be boundary types. Without this, every native method
|
|
155
|
+
# is an entry and must have boundary-crossable signatures.
|
|
156
|
+
def native_entries(*names)
|
|
157
|
+
@__spinel_native.set_entries(names)
|
|
158
|
+
names
|
|
159
|
+
end
|
|
108
160
|
end
|
|
109
161
|
end
|