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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 341a37c7333aabb9a5ca73e8bfc1dfeea3949c9629fdc809c81c34ee83369c43
4
- data.tar.gz: d5a28eafd41ffe17e21c5fee4af88807f1e7103209a163a8e78a88e23d25a6f7
3
+ metadata.gz: 861579157d9270857eede60bc891c64f7b6b78c152a62512ef0ad098f112fad4
4
+ data.tar.gz: 132e7349789d035475fd5386ffa5bf6c60b39a3c08880e9adcd01244563d2e4e
5
5
  SHA512:
6
- metadata.gz: 79161e43d39d1d61384f14675c2b732c21c2b4d34f7ca0e38760ce929c339b04e51b0818d306c83bb74698e03b7bd447c42f1e4f257381a6e549090c5ac2b95c
7
- data.tar.gz: cf96d9b21b54cb6a1adc910caceeced6aa8f14c766b2a70624ec9b13b1775918ba20e1551f154480ae066ba5ebc6ead092bba4d0bdd3d2ebfcffc7492cad9306
6
+ metadata.gz: b195d6cf158a5bc71c802c7f181f9956a868c1989fd3901c6cd2ea93bfd04ddcb40eaf08fcd92e34e09f10253a4403ce2ef4275aa2d084332d6c75a7318ea1f2
7
+ data.tar.gz: 2ed20f171858b8f635bfda4dc66cbad803235430bd162a3cdbc32b3b8dc741acb4638f618c0b361b5eb4d3789b8a3dd094354df457ee6546fc14efe8810edc90
data/CHANGELOG.md CHANGED
@@ -1,6 +1,27 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.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
+ [![Gem Version](https://badge.fury.io/rb/spinel_native.svg)](https://rubygems.org/gems/spinel_native)
4
+ [![CI](https://github.com/khasinski/spinel_native/actions/workflows/ci.yml/badge.svg)](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
- - `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.
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`, 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.
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
- Spinel is not on RubyGems; build it from source once and point the gem at it:
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|
@@ -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|
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Spinel
4
4
  module Native
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spinel_native
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Hasiński