#!/usr/bin/env ruby # Generates C mruby bindings for ALL supported raylib functions/structs/enums # from raylib's official parser output (raylib_api.json). # # ruby gen_raylib.rb # # Supported marshaling: # scalars (int/uint/char/short/long/float/double/bool), const char* (string), # raylib structs by value (wrapped mruby Data objects), and single struct # pointers (passed by reference -> inout). Functions using other pointer kinds # (primitive arrays, void*, char**, callbacks, varargs, pointer/array returns) # are skipped and listed in a comment at the top of the output. require 'json' json_path, out_path, raymath_path = ARGV abort "usage: gen_raylib.rb [raymath_api.json]" unless json_path && out_path # Load a raylib API json, tolerating a known raylib 6.0 bug: the # LoadDirectoryFilesEx description contains literal unescaped double-quotes # ("*.*", "FILES*", "DIRS*") that the parser copies verbatim, breaking strict # JSON. We escape those embedded quotes and retry. Safe on already-valid json: # the escaped form (\"X\") does not contain the unescaped substring "X". 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(json_path) raymath = (raymath_path && File.exist?(raymath_path)) ? load_api(raymath_path) : nil # Functions present in raylib_api.json but not compiled in every raylib platform # build (e.g. PLATFORM_WEB), which would cause link errors. Kept minimal. SKIP_FUNCTIONS = %w[ GetClipboardImage ].to_h { |n| [n, true] } STRUCTS = api['structs'].map { |s| s['name'] } STRUCT_SET = STRUCTS.to_h { |n| [n, true] } ALIASES = api['aliases'].to_h { |a| [a['name'], a['type']] } # Texture2D -> Texture CALLBACKS = api['callbacks'].to_h { |c| [c['name'], true] } INT_TYPES = %w[int char short long size_t int8_t int16_t int32_t int64_t uint8_t uint16_t uint32_t uint64_t].to_h { |t| [t, true] } def base_struct(type) t = type.gsub('const', '').gsub('*', '').strip t = ALIASES[t] || t STRUCT_SET[t] ? t : nil end def pointer?(type) = type.include?('*') # Classify a type for marshaling. Returns [:kind, base_struct_or_nil]. def classify(type, as_return: false) t = type.strip return [:void, nil] if t == 'void' return [:bool, nil] if t == 'bool' return [:float, nil] if t == 'float' || t == 'double' return [:string, nil] if t == 'const char *' || (as_return && t == 'char *') unless pointer?(t) base = t.sub(/^unsigned /, '').sub(/^signed /, '') return [:int, nil] if INT_TYPES[base] || t == 'unsigned int' || t == 'unsigned char' || t == 'unsigned short' || t == 'unsigned long' || base == 'unsigned' s = base_struct(t) return [:struct, s] if s return [:unsupported, nil] end # pointer types return [:unsupported, nil] if t.include?('**') return [:unsupported, nil] if t.include?('(') # function pointer s = base_struct(t) return [:unsupported, nil] if s.nil? || as_return # struct* return unsupported [:structptr, s] end def snake(name) name.gsub(/(\d)([A-Z][a-z])/, '\1_\2') # Vector2Add -> Vector2_Add (keeps Mode2D) .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') .gsub(/([a-z])([A-Z])/, '\1_\2') .downcase end # Ruby method name for a function (Is* -> predicate?) def ruby_method(name) if name =~ /\AIs([A-Z].*)\z/ snake($1) + '?' else snake(name) end end # --- field accessor support --- def field_kind(type) k, s = classify(type) return [k, s] if %i[int float bool].include?(k) return [:struct, s] if k == :struct [:unsupported, nil] end out = +"" out << "/* AUTO-GENERATED by tools/gen_raylib.rb — do not edit. */\n" out << "#include \n#include \n#include \n" out << "#include \n#include \n#include \n" out << "#include \n" out << "#include \n" out << "#include \n" if raymath out << "\n" out << "static void rl_struct_free(mrb_state *mrb, void *p){ if (p) mrb_free(mrb, p); }\n\n" # Per-struct: data type, wrap/ptr helpers (declared early so functions can use them) STRUCTS.each do |name| out << "static const mrb_data_type rl_dt_#{name} = { \"Rl::#{name}\", rl_struct_free };\n" end out << "\n" STRUCTS.each do |name| out << <<~C static mrb_value rl_wrap_#{name}(mrb_state *mrb, #{name} v){ #{name} *p = (#{name}*)mrb_malloc(mrb, sizeof(#{name})); *p = v; struct RClass *m = mrb_module_get(mrb, "Rl"); struct RClass *c = mrb_class_get_under(mrb, m, "#{name}"); return mrb_obj_value(mrb_data_object_alloc(mrb, c, p, &rl_dt_#{name})); } static #{name} *rl_ptr_#{name}(mrb_state *mrb, mrb_value o){ return (#{name}*)mrb_data_get_ptr(mrb, o, &rl_dt_#{name}); } C end out << "\n" # Helper to resolve a struct's helper base name through aliases. def hbase(type) = base_struct(type) # --- struct initialize + field accessors --- struct_defs = api['structs'] struct_defs.each do |st| name = st['name'] fields = st['fields'] supported = fields.map { |f| [f, field_kind(f['type'])] }.select { |_, (k, _)| k != :unsupported } # constructor fmt = +"|" decls = [] assigns = [] argptrs = [] supported.each_with_index do |(f, (k, s)), i| v = "a#{i}" case k when :int then fmt << 'i'; decls << "mrb_int #{v} = 0;"; argptrs << "&#{v}"; assigns << "p->#{f['name']} = (#{f['type']})#{v};" when :float then fmt << 'f'; decls << "mrb_float #{v} = 0;"; argptrs << "&#{v}"; assigns << "p->#{f['name']} = (#{f['type']})#{v};" when :bool then fmt << 'b'; decls << "mrb_bool #{v} = 0;"; argptrs << "&#{v}"; assigns << "p->#{f['name']} = #{v};" when :struct then fmt << 'o'; decls << "mrb_value #{v} = mrb_nil_value();"; argptrs << "&#{v}" assigns << "if (!mrb_nil_p(#{v})) p->#{f['name']} = *rl_ptr_#{hbase(f['type'])}(mrb, #{v});" end end out << "static mrb_value rl_init_#{name}(mrb_state *mrb, mrb_value self){\n" out << " #{name} *p = (#{name}*)mrb_malloc(mrb, sizeof(#{name})); memset(p, 0, sizeof(#{name}));\n" out << " mrb_data_init(self, p, &rl_dt_#{name});\n" unless supported.empty? out << " #{decls.join(' ')}\n" out << " mrb_get_args(mrb, \"#{fmt}\"#{argptrs.empty? ? '' : ', ' + argptrs.join(', ')});\n" out << " #{assigns.join("\n ")}\n" end out << " return self;\n}\n" # accessors supported.each do |(f, (k, s))| fn = f['name'] case k when :int out << "static mrb_value rl_g_#{name}_#{fn}(mrb_state *mrb, mrb_value self){ return mrb_fixnum_value(rl_ptr_#{name}(mrb,self)->#{fn}); }\n" out << "static mrb_value rl_s_#{name}_#{fn}(mrb_state *mrb, mrb_value self){ mrb_int v; mrb_get_args(mrb,\"i\",&v); rl_ptr_#{name}(mrb,self)->#{fn}=(#{f['type']})v; return mrb_fixnum_value(v); }\n" when :float out << "static mrb_value rl_g_#{name}_#{fn}(mrb_state *mrb, mrb_value self){ return mrb_float_value(mrb, rl_ptr_#{name}(mrb,self)->#{fn}); }\n" out << "static mrb_value rl_s_#{name}_#{fn}(mrb_state *mrb, mrb_value self){ mrb_float v; mrb_get_args(mrb,\"f\",&v); rl_ptr_#{name}(mrb,self)->#{fn}=(#{f['type']})v; return mrb_float_value(mrb,v); }\n" when :bool out << "static mrb_value rl_g_#{name}_#{fn}(mrb_state *mrb, mrb_value self){ return mrb_bool_value(rl_ptr_#{name}(mrb,self)->#{fn}); }\n" out << "static mrb_value rl_s_#{name}_#{fn}(mrb_state *mrb, mrb_value self){ mrb_bool v; mrb_get_args(mrb,\"b\",&v); rl_ptr_#{name}(mrb,self)->#{fn}=v; return mrb_bool_value(v); }\n" when :struct hb = hbase(f['type']) out << "static mrb_value rl_g_#{name}_#{fn}(mrb_state *mrb, mrb_value self){ return rl_wrap_#{hb}(mrb, rl_ptr_#{name}(mrb,self)->#{fn}); }\n" out << "static mrb_value rl_s_#{name}_#{fn}(mrb_state *mrb, mrb_value self){ mrb_value v; mrb_get_args(mrb,\"o\",&v); rl_ptr_#{name}(mrb,self)->#{fn}=*rl_ptr_#{hb}(mrb,v); return v; }\n" end end end out << "\n" # --- functions --- skipped = [] fn_regs = [] seen_fns = {} emit_function = lambda do |fn| name = fn['name'] return if seen_fns[name] if SKIP_FUNCTIONS[name] skipped << "#{name} (platform)"; return end seen_fns[name] = true params = fn['params'] || [] rkind, rstruct = classify(fn['returnType'], as_return: true) # skip on unsupported return / params if rkind == :unsupported skipped << "#{name} (ret #{fn['returnType']})"; return end bad = false pinfo = params.map do |p| t = p['type'] if t == '...' || t == 'va_list' || CALLBACKS[t.gsub('*','').strip] bad = true; break end k, s = classify(t) if k == :unsupported bad = true; break end [k, s] end if bad || pinfo.nil? skipped << "#{name} (params)"; return end cname = "rl_fn_#{name}" fmt = +"" decls = [] argptrs = [] callargs = [] pinfo.each_with_index do |(k, s), i| v = "a#{i}" case k when :int then fmt << 'i'; decls << "mrb_int #{v};"; argptrs << "&#{v}"; callargs << "(#{params[i]['type']})#{v}" when :float then fmt << 'f'; decls << "mrb_float #{v};"; argptrs << "&#{v}"; callargs << "(#{params[i]['type']})#{v}" when :bool then fmt << 'b'; decls << "mrb_bool #{v};"; argptrs << "&#{v}"; callargs << "#{v}" when :string then fmt << 'z!'; decls << "const char *#{v} = NULL;"; argptrs << "&#{v}"; callargs << "#{v}" when :struct then fmt << 'o'; decls << "mrb_value #{v};"; argptrs << "&#{v}"; callargs << "(*rl_ptr_#{s}(mrb,#{v}))" when :structptr then fmt << 'o'; decls << "mrb_value #{v};"; argptrs << "&#{v}"; callargs << "rl_ptr_#{s}(mrb,#{v})" end end body = +"" body << "static mrb_value #{cname}(mrb_state *mrb, mrb_value self){\n" body << " #{decls.join(' ')}\n" unless decls.empty? body << " mrb_get_args(mrb, \"#{fmt}\"#{argptrs.empty? ? '' : ', ' + argptrs.join(', ')});\n" unless fmt.empty? call = "#{name}(#{callargs.join(', ')})" case rkind when :void then body << " #{call};\n return mrb_nil_value();\n" when :bool then body << " return mrb_bool_value(#{call});\n" when :int then body << " return mrb_fixnum_value(#{call});\n" when :float then body << " return mrb_float_value(mrb, #{call});\n" when :string then body << " const char *r = #{call};\n return r ? mrb_str_new_cstr(mrb, r) : mrb_nil_value();\n" when :struct then body << " return rl_wrap_#{rstruct}(mrb, #{call});\n" end body << "}\n" out << body fn_regs << %( mrb_define_module_function(mrb, rl, "#{ruby_method(name)}", #{cname}, MRB_ARGS_REQ(#{params.size}));) end api['functions'].each { |fn| emit_function.call(fn) } raymath['functions'].each { |fn| emit_function.call(fn) } if raymath # --- hand-written shader uniform setters --- # SetShaderValue / SetShaderValueV take a `const void *value` + a uniform-type # tag, which the generic marshaller can't express. We accept a Ruby Numeric or # Array of Numerics and pack it into the right C buffer based on `uniform_type`. # (These were in the skip list as "(params)"; remove them now that they're bound.) skipped.reject! { |s| s.start_with?('SetShaderValue ', 'SetShaderValueV ') } out << <<~'C' /* ---- hand-written shader uniform setters ---- */ static int rl_uniform_comps(mrb_int t){ switch (t) { case SHADER_UNIFORM_VEC2: case SHADER_UNIFORM_IVEC2: return 2; case SHADER_UNIFORM_VEC3: case SHADER_UNIFORM_IVEC3: return 3; case SHADER_UNIFORM_VEC4: case SHADER_UNIFORM_IVEC4: return 4; default: return 1; /* FLOAT, INT, SAMPLER2D */ } } static mrb_bool rl_uniform_is_int(mrb_int t){ return (t == SHADER_UNIFORM_INT || t == SHADER_UNIFORM_SAMPLER2D || (t >= SHADER_UNIFORM_IVEC2 && t <= SHADER_UNIFORM_IVEC4)); } /* Pack `count` * `comps` scalars from a Ruby value (scalar, flat Array, or Array of Arrays) into buf. Returns 0 on success, -1 on arity mismatch. */ static int rl_pack_uniform(mrb_state *mrb, mrb_value v, void *buf, int comps, int count, mrb_bool is_int){ float *f = (float*)buf; int *ip = (int*)buf; int total = comps * count, k = 0; if (!mrb_array_p(v)) { if (total != 1) return -1; if (is_int) ip[0] = (int)mrb_as_int(mrb, v); else f[0] = (float)mrb_as_float(mrb, v); return 0; } mrb_int n = RARRAY_LEN(v); for (mrb_int i = 0; i < n; i++) { mrb_value e = mrb_ary_ref(mrb, v, i); if (mrb_array_p(e)) { mrb_int m = RARRAY_LEN(e); for (mrb_int j = 0; j < m; j++, k++) { if (k >= total) return -1; mrb_value s = mrb_ary_ref(mrb, e, j); if (is_int) ip[k] = (int)mrb_as_int(mrb, s); else f[k] = (float)mrb_as_float(mrb, s); } } else { if (k >= total) return -1; if (is_int) ip[k] = (int)mrb_as_int(mrb, e); else f[k] = (float)mrb_as_float(mrb, e); k++; } } return (k == total) ? 0 : -1; } static mrb_value rl_fn_SetShaderValue(mrb_state *mrb, mrb_value self){ mrb_value sh, val; mrb_int loc, utype; mrb_get_args(mrb, "oioi", &sh, &loc, &val, &utype); int comps = rl_uniform_comps(utype); mrb_bool is_int = rl_uniform_is_int(utype); int buf[4]; /* 4 ints or 4 floats, same size */ if (rl_pack_uniform(mrb, val, buf, comps, 1, is_int) != 0) mrb_raisef(mrb, E_ARGUMENT_ERROR, "shader uniform expects %d component(s)", comps); SetShaderValue(*rl_ptr_Shader(mrb, sh), (int)loc, buf, (int)utype); return mrb_nil_value(); } static mrb_value rl_fn_SetShaderValueV(mrb_state *mrb, mrb_value self){ mrb_value sh, val; mrb_int loc, utype, count; mrb_get_args(mrb, "oioii", &sh, &loc, &val, &utype, &count); int comps = rl_uniform_comps(utype); mrb_bool is_int = rl_uniform_is_int(utype); if (count < 1) mrb_raise(mrb, E_ARGUMENT_ERROR, "count must be >= 1"); void *buf = mrb_malloc(mrb, (size_t)comps * (size_t)count * sizeof(int)); int rc = rl_pack_uniform(mrb, val, buf, comps, (int)count, is_int); if (rc != 0) { mrb_free(mrb, buf); mrb_raisef(mrb, E_ARGUMENT_ERROR, "shader uniform array expects %d*%d values", comps, (int)count); } SetShaderValueV(*rl_ptr_Shader(mrb, sh), (int)loc, buf, (int)utype, (int)count); mrb_free(mrb, buf); return mrb_nil_value(); } C fn_regs << %( mrb_define_module_function(mrb, rl, "set_shader_value", rl_fn_SetShaderValue, MRB_ARGS_REQ(4));) fn_regs << %( mrb_define_module_function(mrb, rl, "set_shader_value_v", rl_fn_SetShaderValueV, MRB_ARGS_REQ(5));) # --- enums + defines (constants) --- enum_regs = [] api['enums'].each do |e| e['values'].each do |v| enum_regs << %( mrb_define_const(mrb, rl, "#{v['name']}", mrb_fixnum_value(#{v['value']}));) end end define_regs = [] color_consts = [] api['defines'].each do |d| case d['type'] when 'INT' then define_regs << %( mrb_define_const(mrb, rl, "#{d['name']}", mrb_fixnum_value(#{d['value']}));) when 'FLOAT' then define_regs << %( mrb_define_const(mrb, rl, "#{d['name']}", mrb_float_value(mrb, #{d['value'].to_f}));) when 'STRING' then define_regs << %( mrb_define_const(mrb, rl, "#{d['name']}", mrb_str_new_cstr(mrb, #{d['value'].inspect}));) when 'COLOR' if d['value'] =~ /\{\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\}/ color_consts << %( mrb_define_const(mrb, rl, "#{d['name']}", rl_wrap_Color(mrb, (Color){#{$1},#{$2},#{$3},#{$4}}));) end end end # --- init function --- out << "\nvoid rl_define_generated(mrb_state *mrb){\n" out << " struct RClass *rl = mrb_define_module(mrb, \"Rl\");\n" out << " struct RClass *obj = mrb->object_class;\n (void)obj;\n" # struct classes + accessors struct_defs.each do |st| name = st['name'] out << " {\n struct RClass *c = mrb_define_class_under(mrb, rl, \"#{name}\", mrb->object_class);\n" out << " MRB_SET_INSTANCE_TT(c, MRB_TT_DATA);\n" out << " mrb_define_method(mrb, c, \"initialize\", rl_init_#{name}, MRB_ARGS_OPT(16));\n" fields = st['fields'] fields.each do |f| k, _ = field_kind(f['type']) next if k == :unsupported fn = f['name'] out << " mrb_define_method(mrb, c, \"#{fn}\", rl_g_#{name}_#{fn}, MRB_ARGS_NONE());\n" out << " mrb_define_method(mrb, c, \"#{fn}=\", rl_s_#{name}_#{fn}, MRB_ARGS_REQ(1));\n" end out << " }\n" end out << "\n" out << enum_regs.join("\n") << "\n\n" out << define_regs.join("\n") << "\n\n" out << color_consts.join("\n") << "\n\n" out << fn_regs.join("\n") << "\n" out << "}\n" # Report skipped functions as a comment. total = api['functions'].size + (raymath ? raymath['functions'].size : 0) bound = total - skipped.size header = "/* Generated: #{bound}/#{total} functions bound (raylib#{raymath ? ' + raymath' : ''}). */\n" header << "/* Skipped (#{skipped.size}): #{skipped.join('; ')} */\n\n" File.write(out_path, header + out) warn "gen_raylib: wrote #{out_path} (#{bound}/#{total} functions, #{STRUCTS.size} structs)"