1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
#!/usr/bin/env ruby
# Generates sig/raylib.rbs — RBS type signatures for the raylib + raymath Ruby
# bindings. Reuses the type-mapping machinery (snake, ruby_method, rtype) from
# gen_ai_reference.rb, adapted for RBS syntax.
#
# ruby mrbgems/raylib/tools/gen_rbs.rb
require 'json'
ROOT = File.expand_path('../../..', __dir__)
RAYLIB = File.join(ROOT, 'vendor', 'raylib')
GEN_C = File.join(__dir__, '..', 'src', 'raylib_gen.c')
OUT = File.join(ROOT, 'sig', 'raylib.rbs')
# raylib 6.0 relocated the parser: parser/output/ -> tools/rlparser/output/.
# Tolerant loader for a known raylib 6.0 bug (LoadDirectoryFilesEx description
# has unescaped quotes). Kept in sync with gen_raylib.rb / gen_ai_reference.rb.
def load_api(path)
raw = File.read(path)
return JSON.parse(raw) rescue JSON.parse(raw
.gsub('"*.*"', '\"*.*\"')
.gsub('"FILES*"', '\"FILES*\"')
.gsub('"DIRS*"', '\"DIRS*\"'))
end
API = load_api(File.join(RAYLIB, 'tools/rlparser/output/raylib_api.json'))
RMATH = load_api(File.join(RAYLIB, 'tools/rlparser/output/raymath_api.json'))
ALIASES = API['aliases'].to_h { |a| [a['name'], a['type']] }
STRUCTS = API['structs'].map { |s| s['name'] }.to_h { |n| [n, true] }
def snake(n)
n.gsub(/(\d)([A-Z][a-z])/, '\1_\2').gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
end
def ruby_method(n) = n =~ /\AIs([A-Z].*)\z/ ? snake($1) + '?' : snake(n)
def base_struct(t)
b = t.gsub('const', '').gsub('*', '').strip
b = ALIASES[b] || b
STRUCTS[b] ? b : nil
end
def rtype(t, numeric: false)
s = t.strip
return nil if s == 'void'
return 'Boolean' if s == 'bool'
# mruby's `f` (mrb_float) mrb_get_args format auto-converts Integer -> Float, so
# float *inputs* (params, struct fields) accept ints too. Use `Float | Integer`
# (NOT Numeric): both members declare arithmetic so `v.x * 2` type-checks,
# whereas the abstract Numeric does not declare `*`/`/`/`**`. Returns stay Float:
# the C value really is a Float (pass numeric: true only for inputs).
# WART: RBS's numeric tower widens `**`/some `/` (and camera-math via Math.sin)
# to `Complex`, which then won't fit `Float | Integer` params — surfacing as
# :information ArgumentTypeMismatch under the Steepfile's lenient config (non-
# failing). mruby's auto-coercing numerics don't map cleanly to RBS's strict
# tower; accepted trade-off (see .agents/knowledge/steep.md). Float would reject
# int literals; Numeric breaks arithmetic — Float | Integer is the best fit.
return (numeric ? 'Float | Integer' : 'Float') if s == 'float' || s == 'double'
return 'String' if s == 'const char *' || s == 'char *'
if (b = base_struct(s)) then return "Rl::#{b}" end
return 'Integer' unless s.include?('*')
nil
end
def rbs_type(t, numeric: false)
r = rtype(t, numeric: numeric)
return 'nil' if r.nil? && t.strip == 'void'
return 'untyped' if r.nil?
r == 'Boolean' ? 'bool' : r
end
skip_reason = {}
if File.exist?(GEN_C) && File.read(GEN_C, 4096) =~ /Skipped \(\d+\):\s*(.*?)\*\//m
$1.split(';').each do |e|
e = e.strip
if e =~ /\A([A-Za-z_]\w*)\s*\((.*)\)/ then skip_reason[$1] = $2 end
end
end
skip_reason.delete('SetShaderValue'); skip_reason.delete('SetShaderValueV')
SPECIAL_RBS = {
'SetShaderValue' => ' def self.set_shader_value: (Rl::Shader shader, Integer loc_index, untyped value, Integer uniform_type) -> nil',
'SetShaderValueV' => ' def self.set_shader_value_v: (Rl::Shader shader, Integer loc_index, untyped value, Integer uniform_type, Integer count) -> nil',
}
# Functions emitted only as hand-written sugar below (skip their JSON form): the
# sugar accepts a Symbol key (resolve_key) where the JSON form would be Integer.
SUGAR_OVERRIDE = %w[DrawText DrawTexturePro IsKeyDown IsKeyPressed IsKeyReleased IsKeyUp]
def rbs_sig(fn)
return SPECIAL_RBS[fn['name']] if SPECIAL_RBS[fn['name']]
name = ruby_method(fn['name'])
params = (fn['params'] || []).map { |p|
"#{rbs_type(p['type'], numeric: true)} #{snake(p['name'])}"
}
ret = rbs_type(fn['returnType']) # numeric: false -> Float for float returns
ret = 'nil' if ret.nil?
" def self.#{name}: (#{params.join(', ')}) -> #{ret}"
end
def rbs_struct(st)
fields = st['fields'].map { |f| [f['name'], rbs_type(f['type'], numeric: true)] }.select { |_, t| t && t != 'nil' }
lines = []
lines << "class Rl::#{st['name']}"
fields.each do |name, t|
lines << " attr_accessor #{name}: #{t}"
end
init_params = fields.map { |n, t| "?#{t} #{n}" }
lines << " def initialize: (#{init_params.join(', ')}) -> void"
lines << "end"
lines.join("\n")
end
o = +""
o << "# Generated by gen_rbs.rb — DO NOT EDIT.\n"
o << "# Regenerate: ruby mrbgems/raylib/tools/gen_rbs.rb\n\n"
# --- module Rl: functions ---
o << "module Rl\n"
fns = (API['functions'] + RMATH['functions']).reject { |f| skip_reason[f['name']] || SUGAR_OVERRIDE.include?(f['name']) }
fns.each { |f| o << rbs_sig(f) << "\n" }
# --- Ruby sugar (hand-written, not from JSON) ---
o << "\n # --- Ruby sugar (mrblib/raylib.rb) ---\n"
o << " def self.while_window_open: () { -> void } -> void\n"
o << " def self.window_should_close?: () -> bool\n"
o << " def self.draw: (?clear_color: Rl::Color) { -> void } -> void\n"
o << " def self.mode_2d: (Rl::Camera2D camera) { -> void } -> void\n"
o << " def self.mode_3d: (Rl::Camera3D camera) { -> void } -> void\n"
o << " def self.texture_mode: (Rl::RenderTexture target) { -> void } -> void\n"
o << " def self.blend_mode: (Integer mode) { -> void } -> void\n"
o << " def self.shader_mode: (Rl::Shader shader) { -> void } -> void\n"
o << " def self.scissor_mode: (Integer x, Integer y, Integer width, Integer height) { -> void } -> void\n"
o << " def self.draw_text: (text: String, x: Integer, y: Integer, font_size: Integer, color: Rl::Color) -> nil\n"
o << " def self.draw_texture_pro: (texture: Rl::Texture, source: Rl::Rectangle, dest: Rl::Rectangle, ?origin: Rl::Vector2, ?rotation: Float, ?tint: Rl::Color) -> nil\n"
o << " def self.target_fps=: (Integer) -> Integer\n"
o << " def self.master_volume=: (Float) -> Float\n"
o << " def self.frame_time: () -> Float\n"
o << " def self.time: () -> Float\n"
o << " def self.fps: () -> Integer\n"
o << " def self.screen_width: () -> Integer\n"
o << " def self.screen_height: () -> Integer\n"
o << " def self.mouse_x: () -> Integer\n"
o << " def self.mouse_y: () -> Integer\n"
o << " def self.mouse_position: () -> Rl::Vector2\n"
o << " def self.mouse_wheel: () -> Float\n"
o << " def self.platform: () -> Symbol\n"
o << " def self.web?: () -> bool\n"
o << " def self.desktop?: () -> bool\n"
o << " def self.key_down?: (untyped key) -> bool\n"
o << " def self.key_pressed?: (untyped key) -> bool\n"
o << " def self.key_released?: (untyped key) -> bool\n"
o << " def self.key_up?: (untyped key) -> bool\n"
# --- constants (enums + defines) ---
o << "\n # --- Constants ---\n"
API['enums'].each do |e|
e['values'].each { |v| o << " #{v['name']}: Integer\n" }
end
colors = []; ints = []; floats = []; strings = []
API['defines'].each do |d|
case d['type']
when 'COLOR' then colors << d['name']
when 'INT' then ints << d['name']
when 'FLOAT' then floats << d['name']
when 'STRING' then strings << d['name']
end
end
colors.each { |c| o << " #{c}: Rl::Color\n" }
(ints + floats).each { |c| o << " #{c}: #{floats.include?(c) ? 'Float' : 'Integer'}\n" }
strings.each { |c| o << " #{c}: String\n" }
# --- aliases (Texture2D = Texture, etc.) ---
# raylib 6.0 adds a pointer typedef `typedef Transform *ModelAnimPose;` whose
# alias name comes through as "*ModelAnimPose" — strip a leading '*' so the RBS
# identifier stays valid.
API['aliases'].each do |a|
name = a['name'].to_s.sub(/\A\*/, '')
o << " #{name}: untyped # alias for Rl::#{a['type']}\n"
end
o << "end\n\n"
# --- structs as classes ---
API['structs'].each do |st|
o << rbs_struct(st) << "\n\n"
end
File.write(OUT, o)
warn "wrote #{OUT}: #{fns.size} functions, #{API['structs'].size} structs, " \
"#{API['enums'].sum { |e| e['values'].size } + colors.size + ints.size + floats.size + strings.size} constants"
|