badline 0.6.0 → 0.6.1

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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/CLAUDE.md +10 -9
  4. data/README.md +24 -10
  5. data/doc/pinned-behaviour.md +131 -30
  6. data/exe/badline-ruby +20 -24
  7. data/lib/badline/address_bus.rb +10 -1
  8. data/lib/badline/audio/cli.rb +18 -5
  9. data/lib/badline/audio/console.rb +2 -1
  10. data/lib/badline/audio.rb +0 -3
  11. data/lib/badline/computer.rb +6 -3
  12. data/lib/badline/cpu/boundary.rb +54 -0
  13. data/lib/badline/cpu/core.rb +165 -0
  14. data/lib/badline/cpu.rb +3 -214
  15. data/lib/badline/drive1541/cpu.rb +29 -0
  16. data/lib/badline/drive1541.rb +1 -0
  17. data/lib/badline/ffi.rb +157 -0
  18. data/lib/badline/frontend/app.rb +225 -0
  19. data/lib/badline/frontend/audio_sink.rb +72 -0
  20. data/lib/badline/frontend/boot.rb +52 -0
  21. data/lib/badline/frontend/controls.rb +163 -0
  22. data/lib/badline/frontend/drive_led.rb +51 -0
  23. data/lib/badline/frontend/frame_rate.rb +83 -0
  24. data/lib/badline/frontend/gamepads.rb +76 -0
  25. data/lib/badline/frontend/keys.rb +48 -0
  26. data/lib/badline/frontend/pacer.rb +100 -0
  27. data/lib/badline/frontend/pad_port.rb +61 -0
  28. data/lib/badline/frontend/screen.rb +65 -0
  29. data/lib/badline/frontend/screenshot.rb +22 -0
  30. data/lib/badline/frontend/snapshots.rb +60 -0
  31. data/lib/badline/frontend/sound.rb +153 -0
  32. data/lib/badline/frontend/timeline.rb +171 -0
  33. data/lib/badline/frontend.rb +18 -0
  34. data/lib/badline/options/event.rb +94 -0
  35. data/lib/badline/options/help.rb +78 -0
  36. data/lib/badline/options/option.rb +29 -0
  37. data/lib/badline/options/table.rb +62 -0
  38. data/lib/badline/options.rb +181 -145
  39. data/lib/badline/reu/dma/saved_state.rb +50 -0
  40. data/lib/badline/reu/dma.rb +56 -47
  41. data/lib/badline/reu/saved_state.rb +2 -1
  42. data/lib/badline/reu.rb +19 -10
  43. data/lib/badline/sdl.rb +123 -51
  44. data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
  45. data/lib/badline/sid/waveform.rb +2 -3
  46. data/lib/badline/snapshot/state.rb +1 -1
  47. data/lib/badline/version.rb +1 -1
  48. data/lib/badline/vic/bank.rb +21 -0
  49. data/lib/badline/vic.rb +15 -6
  50. metadata +26 -19
  51. data/.claude/hooks/session-start.sh +0 -57
  52. data/.claude/settings.json +0 -14
  53. data/lib/badline/audio/sdl_sink.rb +0 -70
  54. data/lib/badline/audio/stream.rb +0 -123
  55. data/lib/badline/gui/application.rb +0 -243
  56. data/lib/badline/gui/drive_led_pane.rb +0 -29
  57. data/lib/badline/gui/gamepads.rb +0 -104
  58. data/lib/badline/gui/joy_map.rb +0 -27
  59. data/lib/badline/gui/key_map.rb +0 -39
  60. data/lib/badline/gui/palette.rb +0 -35
  61. data/lib/badline/gui/pane.rb +0 -36
  62. data/lib/badline/gui/screen_pane.rb +0 -46
  63. data/lib/badline/gui/snapshots.rb +0 -67
  64. data/lib/badline/gui/window.rb +0 -61
  65. data/lib/badline/gui.rb +0 -14
  66. data/lib/badline/sdl/events.rb +0 -49
  67. data/lib/badline/sdl/functions.rb +0 -74
  68. data/vendor/.gitkeep +0 -0
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/cpu/microcode"
4
+ require "badline/cpu/addressing"
5
+ require "badline/cpu/operations"
6
+ require "badline/cpu/stack_operations"
7
+ require "badline/cpu/boundary"
8
+ require "badline/cpu/saved_state"
9
+
10
+ module Badline
11
+ class CPU
12
+ STATUS_FLAGS = [:carry, :zero, :interrupt, :decimal, :break, 1, :overflow, :negative].freeze
13
+
14
+ # The 6502 core, on whatever bus +memory+ is. The C64's 6510 (CPU) and
15
+ # the 1541's 6502 (Drive1541::CPU) each include it.
16
+ module Core
17
+ include IntegerHelper
18
+ include InstructionSet
19
+ include Interrupts
20
+ include Traps
21
+ include Addressing
22
+ include Operations
23
+ include StackOperations
24
+ include Boundary
25
+ include SavedState
26
+
27
+ attr_reader :memory, :instructions, :boundary_crossed, :cycles, :pending_write
28
+ attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y, :nmi, :irq
29
+
30
+ alias pending_write? pending_write
31
+
32
+ # +ane_constant+ is ANE's magic constant, which varies from chip to
33
+ # chip. The default is the C64 6510's.
34
+ def initialize(memory, debug: false, ane_constant: 0xef)
35
+ @debug = debug
36
+ @ane_constant = ane_constant
37
+ @memory = memory
38
+ @status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
39
+ reset_registers
40
+
41
+ @nmi = @irq = false
42
+ @irq_sample = @irq_pending = false
43
+ @nmi_sample = @nmi_pending = false
44
+ @skip_poll = false
45
+ @boundary_crossed = false
46
+ @interrupt = nil
47
+ @brk = false
48
+ @pending_write = false
49
+ @stalled_at = nil
50
+ @so_high = true
51
+
52
+ @cycles = 0
53
+ @instructions = 0
54
+ @traps = nil
55
+ end_sequence
56
+ end
57
+
58
+ def reset!
59
+ status.interrupt = true
60
+ reset_registers
61
+ @nmi = false
62
+ @irq_sample = @irq_pending = false
63
+ @nmi_sample = @nmi_pending = false
64
+ @skip_poll = false
65
+ @pending_write = false
66
+ end_sequence
67
+ end
68
+
69
+ def p
70
+ status.value
71
+ end
72
+
73
+ def p=(new_value)
74
+ status.value = new_value
75
+ end
76
+
77
+ def cycle!
78
+ poll # the interrupt lines
79
+
80
+ # Run the next step
81
+ index = @index
82
+ @index = index + 1
83
+ send(@plan[index])
84
+ @cycles += 1
85
+
86
+ # Record if the next step writes to memory
87
+ @pending_write = @writes[@index]
88
+ nil
89
+ end
90
+
91
+ # Called instead of #cycle! on a cycle the VIC holds the CPU through
92
+ # BA. Records which cycle the CPU was stalled before, and keeps
93
+ # sampling the interrupt lines.
94
+ def stall!
95
+ @stalled_at = @cycles
96
+ sample_while_stalled
97
+ end
98
+
99
+ # The 6502's SO (set overflow) pin, which the 6510 leaves out, so the
100
+ # C64 never drives it. A falling edge sets V and a held level does
101
+ # nothing more.
102
+ def so=(high)
103
+ @status.overflow = true if @so_high && !high
104
+ @so_high = high
105
+ end
106
+
107
+ # Pulses SO: pulls it low, which sets V, and releases it.
108
+ def so!
109
+ @status.overflow = true
110
+ @so_high = true
111
+ end
112
+
113
+ def jammed?
114
+ @plan.equal?(JAMMED_PLAN)
115
+ end
116
+
117
+ # Runs to the end of the instruction, or until the CPU jams.
118
+ def step!
119
+ cycle!
120
+ cycle! until @plan.equal?(FETCH_PLAN) || jammed?
121
+ nil
122
+ end
123
+
124
+ # Whether the next cycle fetches an opcode.
125
+ def boundary? = @index.zero? && @plan.equal?(FETCH_PLAN)
126
+
127
+ def trapped? = !@traps.nil?
128
+
129
+ def inspect
130
+ "Cycles: #{@cycles}, PC: #{format16(program_counter)}, " \
131
+ "SP: #{format8(stack_pointer)}, A: #{format8(a)}, X: #{format8(x)}, " \
132
+ "Y: #{format8(y)}, P: #{format8(p)}"
133
+ end
134
+
135
+ private
136
+
137
+ # True when the CPU was stalled right before this cycle.
138
+ def stalled_before_this_cycle?
139
+ @stalled_at == @cycles
140
+ end
141
+
142
+ # True when the CPU was stalled right before the cycle that preceded
143
+ # this one.
144
+ def stalled_before_previous_cycle?
145
+ @stalled_at == @cycles - 1
146
+ end
147
+
148
+ def write_byte(addr, value)
149
+ @memory.poke(addr, value)
150
+ end
151
+
152
+ def stack_address(offset = 0)
153
+ 0x0100 | ((@stack_pointer + offset) & 0xff)
154
+ end
155
+
156
+ def reset_registers
157
+ # The program counter is initialized from the reset vector
158
+ @program_counter = @memory.peek16(0xfffc)
159
+ # Stack pointer starts at 0x01ff and grows down
160
+ @stack_pointer = 0xff
161
+ @a = @x = @y = 0x0
162
+ end
163
+ end
164
+ end
165
+ end
data/lib/badline/cpu.rb CHANGED
@@ -1,221 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "badline/cpu/microcode"
4
- require "badline/cpu/addressing"
5
- require "badline/cpu/operations"
6
- require "badline/cpu/stack_operations"
7
- require "badline/cpu/saved_state"
3
+ require "badline/cpu/core"
8
4
 
9
5
  module Badline
6
+ # The C64's 6510, on the AddressBus.
10
7
  class CPU
11
- STATUS_FLAGS = [:carry, :zero, :interrupt, :decimal, :break, 1, :overflow, :negative].freeze
12
-
13
- include IntegerHelper
14
- include InstructionSet
15
- include Interrupts
16
- include Traps
17
- include Addressing
18
- include Operations
19
- include StackOperations
20
- include SavedState
21
-
22
- attr_reader :memory, :instructions, :boundary_crossed, :cycles, :pending_write
23
- attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y, :nmi, :irq
24
-
25
- alias pending_write? pending_write
26
-
27
- # +ane_constant+ is ANE's magic constant, which varies from chip to
28
- # chip. The default is the C64 6510's.
29
- def initialize(memory, debug: false, ane_constant: 0xef)
30
- @debug = debug
31
- @ane_constant = ane_constant
32
- @memory = memory
33
- @status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
34
- reset_registers
35
-
36
- @nmi = @irq = false
37
- @irq_sample = @irq_pending = false
38
- @nmi_sample = @nmi_pending = false
39
- @skip_poll = false
40
- @boundary_crossed = false
41
- @interrupt = nil
42
- @brk = false
43
- @pending_write = false
44
- @stalled_at = nil
45
- @so_high = true
46
-
47
- @cycles = 0
48
- @instructions = 0
49
- @traps = nil
50
- end_sequence
51
- end
52
-
53
- def reset!
54
- status.interrupt = true
55
- reset_registers
56
- @nmi = false
57
- @irq_sample = @irq_pending = false
58
- @nmi_sample = @nmi_pending = false
59
- @skip_poll = false
60
- @pending_write = false
61
- end_sequence
62
- end
63
-
64
- def p
65
- status.value
66
- end
67
-
68
- def p=(new_value)
69
- status.value = new_value
70
- end
71
-
72
- def cycle!
73
- poll # the interrupt lines
74
-
75
- # Run the next step
76
- index = @index
77
- @index = index + 1
78
- send(@plan[index])
79
- @cycles += 1
80
-
81
- # Record if the next step writes to memory
82
- @pending_write = @writes[@index]
83
- nil
84
- end
85
-
86
- # Called instead of #cycle! on a cycle the VIC holds the CPU through
87
- # BA. Records which cycle the CPU was stalled before, and keeps
88
- # sampling the interrupt lines.
89
- def stall!
90
- @stalled_at = @cycles
91
- sample_while_stalled
92
- end
93
-
94
- # The 6502's SO (set overflow) pin, which the 6510 leaves out, so the
95
- # C64 never drives it. A falling edge sets V and a held level does
96
- # nothing more.
97
- def so=(high)
98
- @status.overflow = true if @so_high && !high
99
- @so_high = high
100
- end
101
-
102
- # Pulses SO: pulls it low, which sets V, and releases it.
103
- def so!
104
- @status.overflow = true
105
- @so_high = true
106
- end
107
-
108
- def jammed?
109
- @plan.equal?(JAMMED_PLAN)
110
- end
111
-
112
- # Runs to the end of the instruction, or until the CPU jams.
113
- def step!
114
- cycle!
115
- cycle! until @plan.equal?(FETCH_PLAN) || jammed?
116
- nil
117
- end
118
-
119
- # Whether the next cycle fetches an opcode.
120
- def boundary? = @index.zero? && @plan.equal?(FETCH_PLAN)
121
-
122
- def trapped? = !@traps.nil?
123
-
124
- # Everything the CPU holds but its cycle and instruction counts: the
125
- # registers, the interrupt lines and pipeline, and what the last
126
- # instruction left in its working registers. Drive1541::Idle compares
127
- # it at two instruction boundaries.
128
- def idle_state
129
- [@program_counter, @stack_pointer, @a, @x, @y, @status.value,
130
- @irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
131
- @boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
132
- @operation, @optional_dummy, @boundary_crossed, @branch_taken,
133
- @address, @pointer, @dummy_address, @value, @rmw_result]
134
- end
135
-
136
- # Counts +cycles+ cycles and +instructions+ instructions that ran
137
- # without changing anything idle_state holds.
138
- def fast_forward(cycles, instructions)
139
- @cycles += cycles
140
- @instructions += instructions
141
- end
142
-
143
- def inspect
144
- "Cycles: #{@cycles}, PC: #{format16(program_counter)}, " \
145
- "SP: #{format8(stack_pointer)}, A: #{format8(a)}, X: #{format8(x)}, " \
146
- "Y: #{format8(y)}, P: #{format8(p)}"
147
- end
148
-
149
- private
150
-
151
- # The first cycle of every instruction. Starts an interrupt if one was
152
- # pending when the previous instruction ended. Otherwise reads the
153
- # opcode and switches to its plan.
154
- def op_fetch
155
- return start_interrupt if @boundary_nmi || @boundary_irq
156
-
157
- run_traps
158
- opcode = @memory.peek(@program_counter)
159
- log(opcode) if @debug
160
- micro = MICROCODE[opcode]
161
- @operation = micro.operation
162
- @plan = micro.plan
163
- @writes = micro.writes
164
- @optional_dummy = micro.optional_dummy
165
- @index = 1
166
- @boundary_crossed = false
167
- @program_counter = (@program_counter + 1) & 0xffff
168
- end
169
-
170
- def end_instruction
171
- @instructions += 1
172
- end_sequence
173
- end
174
-
175
- # Makes the next cycle an opcode fetch, and saves the pending interrupt
176
- # flags for op_fetch. The next cycle polls before op_fetch runs, which
177
- # updates the live flags.
178
- def end_sequence
179
- @plan = FETCH_PLAN
180
- @writes = FETCH_WRITES
181
- @index = 0
182
- @boundary_irq = @irq_pending
183
- @boundary_nmi = @nmi_pending
184
- end
185
-
186
- # True when the CPU was stalled right before this cycle.
187
- def stalled_before_this_cycle?
188
- @stalled_at == @cycles
189
- end
190
-
191
- # True when the CPU was stalled right before the cycle that preceded
192
- # this one.
193
- def stalled_before_previous_cycle?
194
- @stalled_at == @cycles - 1
195
- end
196
-
197
- def write_byte(addr, value)
198
- @memory.poke(addr, value)
199
- end
200
-
201
- def stack_address(offset = 0)
202
- 0x0100 | ((@stack_pointer + offset) & 0xff)
203
- end
204
-
205
- def reset_registers
206
- # The program counter is initialized from the reset vector
207
- @program_counter = @memory.peek16(0xfffc)
208
- # Stack pointer starts at 0x01ff and grows down
209
- @stack_pointer = 0xff
210
- @a = @x = @y = 0x0
211
- end
212
-
213
- def log(opcode)
214
- instruction = Instruction.find(opcode)
215
- puts(
216
- "#{@cycles}: PC: #{format16(@program_counter)} - " \
217
- "#{instruction.name.upcase} #{instruction.addressing_mode}"
218
- )
219
- end
8
+ include Core
220
9
  end
221
10
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1541
5
+ # The drive's 6502, on the drive's Bus.
6
+ class CPU
7
+ include Badline::CPU::Core
8
+
9
+ # Everything the CPU holds but its cycle and instruction counts: the
10
+ # registers, the interrupt lines and pipeline, and what the last
11
+ # instruction left in its working registers. Drive1541::Idle compares
12
+ # it at two instruction boundaries.
13
+ def idle_state
14
+ [@program_counter, @stack_pointer, @a, @x, @y, @status.value,
15
+ @irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
16
+ @boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
17
+ @operation, @optional_dummy, @boundary_crossed, @branch_taken,
18
+ @address, @pointer, @dummy_address, @value, @rmw_result]
19
+ end
20
+
21
+ # Counts +cycles+ cycles and +instructions+ instructions that ran
22
+ # without changing anything idle_state holds.
23
+ def fast_forward(cycles, instructions)
24
+ @cycles += cycles
25
+ @instructions += instructions
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/drive1541/bus"
4
+ require "badline/drive1541/cpu"
4
5
  require "badline/drive1541/serial_port"
5
6
  require "badline/drive1541/gcr"
6
7
  require "badline/drive1541/track"
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle"
4
+
5
+ module Badline
6
+ # The part of Spinel's FFI DSL the SDL binding uses, for CRuby, over
7
+ # Fiddle, so the one binding in badline/sdl serves both builds. Spinel
8
+ # compiles the declarations itself and never loads this file: require it
9
+ # before a file that declares a binding.
10
+ #
11
+ # The declarations behave as they do in Spinel. ffi_func defines
12
+ # Module.name(...); a pointer argument takes nil for NULL, a Fiddle
13
+ # pointer, a String (its bytes) or an IO::Buffer, whose contents C sees
14
+ # for the call's duration; a NULL pointer comes back as nil. ffi_const
15
+ # sets Module::NAME, ffi_buffer defines Module.name returning a zeroed
16
+ # static buffer, and ffi_read_<width> and ffi_write_<width> define field
17
+ # accessors at a fixed offset into a pointer.
18
+ module FFI
19
+ class Error < StandardError; end
20
+
21
+ TYPES = {
22
+ void: Fiddle::TYPE_VOID,
23
+ int: Fiddle::TYPE_INT, bool: Fiddle::TYPE_INT, long: Fiddle::TYPE_LONG, size_t: Fiddle::TYPE_SIZE_T,
24
+ int8: Fiddle::TYPE_INT8_T, uint8: Fiddle::TYPE_UINT8_T, int16: Fiddle::TYPE_INT16_T,
25
+ uint16: Fiddle::TYPE_UINT16_T, int32: Fiddle::TYPE_INT32_T, uint32: Fiddle::TYPE_UINT32_T,
26
+ float: Fiddle::TYPE_FLOAT, double: Fiddle::TYPE_DOUBLE,
27
+ str: Fiddle::TYPE_CONST_STRING,
28
+ ptr: Fiddle::TYPE_VOIDP, buffer_in: Fiddle::TYPE_VOIDP, buffer_out: Fiddle::TYPE_VOIDP,
29
+ buffer_inout: Fiddle::TYPE_VOIDP, int_array: Fiddle::TYPE_VOIDP, float_array: Fiddle::TYPE_VOIDP
30
+ }.freeze
31
+
32
+ # The argument specs C may write through, so an IO::Buffer passed in one
33
+ # gets back what C left in it.
34
+ WRITABLE = %i[ptr buffer_out buffer_inout].freeze
35
+
36
+ # pack directives for the accessor widths, in the host's byte order.
37
+ WIDTHS = {
38
+ u8: "C", i8: "c", u16: "S", i16: "s", u32: "L", i32: "l", u64: "Q", i64: "q", ptr: "J"
39
+ }.freeze
40
+
41
+ # Where dlopen looks for a library besides its search path, which leaves
42
+ # out Homebrew's, and the versioned names a runtime package installs
43
+ # without the development package's unversioned link.
44
+ DIRECTORIES = [nil, "/opt/homebrew/lib", "/usr/local/lib"].freeze
45
+ VERSIONED = { "SDL2" => %w[libSDL2-2.0.so.0 libSDL2-2.0.0.dylib] }.freeze
46
+
47
+ module_function
48
+
49
+ def open_library(name)
50
+ candidates = library_candidates(name)
51
+ candidates.each do |candidate|
52
+ return Fiddle.dlopen(candidate)
53
+ rescue Fiddle::DLError
54
+ next
55
+ end
56
+ raise Error, "can't find lib#{name} (tried #{candidates.join(', ')})"
57
+ end
58
+
59
+ def library_candidates(name)
60
+ names = ["lib#{name}.dylib", "lib#{name}.so", *VERSIONED.fetch(name, []), "#{name}.dll"]
61
+ DIRECTORIES.flat_map { |dir| names.map { |file| dir ? File.join(dir, file) : file } }
62
+ end
63
+
64
+ # The value Fiddle passes for one argument, and a block that copies what
65
+ # C wrote back into an IO::Buffer once the call returns.
66
+ def argument(spec, value)
67
+ case value
68
+ when IO::Buffer then buffer_argument(spec, value)
69
+ when Array then [value.pack(spec == :float_array ? "d*" : "q*")]
70
+ when true, false then [value ? 1 : 0]
71
+ else [value]
72
+ end
73
+ end
74
+
75
+ def buffer_argument(spec, buffer)
76
+ return [nil] if buffer.null? || buffer.empty?
77
+
78
+ bytes = buffer.get_string
79
+ return [bytes] unless WRITABLE.include?(spec) && !buffer.readonly?
80
+
81
+ [bytes, -> { buffer.set_string(bytes) }]
82
+ end
83
+
84
+ def result(spec, value)
85
+ case spec
86
+ when :ptr then value.null? ? nil : value
87
+ when :bool then value != 0
88
+ else value
89
+ end
90
+ end
91
+
92
+ def read(pointer, offset, width)
93
+ value = Fiddle::Pointer[pointer][offset, width_size(width)].unpack1(WIDTHS.fetch(width))
94
+ width == :ptr ? result(:ptr, Fiddle::Pointer.new(value)) : value
95
+ end
96
+
97
+ def write(pointer, offset, width, value)
98
+ value = value.to_i if width == :ptr
99
+ Fiddle::Pointer[pointer][offset, width_size(width)] = [value || 0].pack(WIDTHS.fetch(width))
100
+ value
101
+ end
102
+
103
+ def width_size(width) = [0].pack(WIDTHS.fetch(width)).bytesize
104
+
105
+ # The declarations, private methods of every module once this file is
106
+ # loaded, as they are keywords of a module body in Spinel.
107
+ module DSL
108
+ private
109
+
110
+ def ffi_lib(name)
111
+ (@ffi_libraries ||= []) << FFI.open_library(name)
112
+ end
113
+
114
+ def ffi_func(name, arguments, returns, blocking: false)
115
+ function = Fiddle::Function.new(ffi_symbol(name), arguments.map { |spec| TYPES.fetch(spec) },
116
+ TYPES.fetch(returns), name: name.to_s, need_gvl: !blocking)
117
+ define_singleton_method(name) do |*values|
118
+ converted = arguments.zip(values).map { |spec, value| FFI.argument(spec, value) }
119
+ returned = function.call(*converted.map(&:first))
120
+ converted.each { |argument| argument[1]&.call }
121
+ FFI.result(returns, returned)
122
+ end
123
+ end
124
+
125
+ def ffi_const(name, value)
126
+ const_set(name, value)
127
+ end
128
+
129
+ def ffi_buffer(name, size)
130
+ buffer = Fiddle::Pointer.malloc(size, Fiddle::RUBY_FREE)
131
+ buffer[0, size] = "\0" * size
132
+ define_singleton_method(name) { buffer }
133
+ end
134
+
135
+ WIDTHS.each_key do |width|
136
+ define_method(:"ffi_read_#{width}") do |name, offset|
137
+ define_singleton_method(name) { |pointer| FFI.read(pointer, offset, width) }
138
+ end
139
+
140
+ define_method(:"ffi_write_#{width}") do |name, offset|
141
+ define_singleton_method(name) { |pointer, value| FFI.write(pointer, offset, width, value) }
142
+ end
143
+ end
144
+
145
+ def ffi_symbol(name)
146
+ (@ffi_libraries || []).each do |library|
147
+ return library[name.to_s]
148
+ rescue Fiddle::DLError
149
+ next
150
+ end
151
+ Fiddle::Handle::DEFAULT[name.to_s]
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ Module.include(Badline::FFI::DSL)