rbxx 0.1.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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +72 -0
  3. data/exe/rbxx +6 -0
  4. data/ext-cmake/FindRuby.cmake +42 -0
  5. data/ext-cmake/rbxx-config.cmake +25 -0
  6. data/include/rbxx/arg.hpp +347 -0
  7. data/include/rbxx/callback.hpp +54 -0
  8. data/include/rbxx/class.hpp +630 -0
  9. data/include/rbxx/data_object.hpp +221 -0
  10. data/include/rbxx/detail/ruby_include.hpp +36 -0
  11. data/include/rbxx/exception.hpp +116 -0
  12. data/include/rbxx/extension.hpp +23 -0
  13. data/include/rbxx/function.hpp +463 -0
  14. data/include/rbxx/module.hpp +69 -0
  15. data/include/rbxx/nogvl.hpp +146 -0
  16. data/include/rbxx/object.hpp +68 -0
  17. data/include/rbxx/operators.hpp +31 -0
  18. data/include/rbxx/policies.hpp +36 -0
  19. data/include/rbxx/protect.hpp +72 -0
  20. data/include/rbxx/rbxx.hpp +20 -0
  21. data/include/rbxx/registry.hpp +145 -0
  22. data/include/rbxx/stl/array.hpp +40 -0
  23. data/include/rbxx/stl/chrono.hpp +26 -0
  24. data/include/rbxx/stl/detail.hpp +60 -0
  25. data/include/rbxx/stl/filesystem.hpp +21 -0
  26. data/include/rbxx/stl/map.hpp +49 -0
  27. data/include/rbxx/stl/optional.hpp +23 -0
  28. data/include/rbxx/stl/set.hpp +30 -0
  29. data/include/rbxx/stl/tuple.hpp +74 -0
  30. data/include/rbxx/stl/variant.hpp +35 -0
  31. data/include/rbxx/stl/vector.hpp +34 -0
  32. data/include/rbxx/stl.hpp +11 -0
  33. data/include/rbxx/type_caster.hpp +247 -0
  34. data/include/rbxx/value.hpp +63 -0
  35. data/include/rbxx/version.hpp +14 -0
  36. data/lib/rbxx/cli.rb +127 -0
  37. data/lib/rbxx/mkmf.rb +144 -0
  38. data/lib/rbxx/rake_tasks.rb +44 -0
  39. data/lib/rbxx/version.rb +5 -0
  40. data/lib/rbxx.rb +8 -0
  41. data/single_include/rbxx/rbxx.hpp +3211 -0
  42. data/templates/CMakeLists.txt.erb +14 -0
  43. data/templates/Gemfile.erb +10 -0
  44. data/templates/README.md.erb +11 -0
  45. data/templates/Rakefile.erb +23 -0
  46. data/templates/ci.yml.erb +21 -0
  47. data/templates/extconf.rb.erb +5 -0
  48. data/templates/extconf_cmake.rb.erb +5 -0
  49. data/templates/extension.cpp.erb +23 -0
  50. data/templates/gemspec.erb +16 -0
  51. data/templates/lib.rb.erb +4 -0
  52. data/templates/release.yml.erb +21 -0
  53. data/templates/test.rb.erb +12 -0
  54. data/templates/version.rb.erb +5 -0
  55. metadata +99 -0
@@ -0,0 +1,3211 @@
1
+ // Generated by scripts/amalgamate.rb. Do not edit directly.
2
+ #pragma once
3
+
4
+ // BEGIN rbxx/detail/ruby_include.hpp
5
+
6
+ #if defined(_MSC_VER)
7
+ #ifndef NOMINMAX
8
+ #define NOMINMAX
9
+ #endif
10
+ #pragma warning(push)
11
+ #pragma warning(disable : 4127 4244 4267 4996)
12
+ #else
13
+ #pragma GCC diagnostic push
14
+ #pragma GCC diagnostic ignored "-Wconversion"
15
+ #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
16
+ #pragma GCC diagnostic ignored "-Wsign-conversion"
17
+ #pragma GCC diagnostic ignored "-Wunused-parameter"
18
+ #endif
19
+
20
+ #include <ruby.h>
21
+ #include <ruby/thread.h>
22
+ #include <ruby/version.h>
23
+
24
+ #if defined(_MSC_VER)
25
+ #pragma warning(pop)
26
+ #else
27
+ #pragma GCC diagnostic pop
28
+ #endif
29
+
30
+ #define RBXX_RUBY_VERSION_MAJOR RUBY_API_VERSION_MAJOR
31
+ #define RBXX_RUBY_VERSION_MINOR RUBY_API_VERSION_MINOR
32
+ #define RBXX_RUBY_VERSION_TEENY RUBY_API_VERSION_TEENY
33
+
34
+ #if RUBY_API_VERSION_MAJOR < 4
35
+ // CRuby exports this debug helper before 4.0 but does not declare it in ruby/thread.h.
36
+ extern "C" int ruby_thread_has_gvl_p(void);
37
+ #endif
38
+
39
+ static_assert(RUBY_API_VERSION_MAJOR >= 3, "rbxx requires CRuby 3.1 or newer");
40
+ // END rbxx/detail/ruby_include.hpp
41
+
42
+ // BEGIN rbxx/value.hpp
43
+
44
+
45
+ #include <type_traits>
46
+
47
+ namespace rbxx {
48
+
49
+ /// @brief A zero-overhead, non-owning view of a Ruby VALUE.
50
+ /// @code rbxx::value nil{Qnil}; @endcode
51
+ class value {
52
+ public:
53
+ /// @brief Constructs a nil value.
54
+ constexpr value() noexcept = default;
55
+
56
+ /// @brief Wraps a raw Ruby VALUE without pinning it.
57
+ /// @code auto wrapped = rbxx::value{Qtrue}; @endcode
58
+ explicit constexpr value(VALUE raw) noexcept : raw_(raw) {}
59
+
60
+ /// @brief Returns the wrapped Ruby VALUE.
61
+ /// @code VALUE raw = wrapped.raw(); @endcode
62
+ [[nodiscard]] constexpr VALUE raw() const noexcept { return raw_; }
63
+
64
+ /// @brief Returns whether this value is nil.
65
+ /// @code if (wrapped.is_nil()) { return; } @endcode
66
+ [[nodiscard]] constexpr bool is_nil() const noexcept { return NIL_P(raw_); }
67
+
68
+ /// @brief Returns whether this value is a Ruby boolean.
69
+ [[nodiscard]] constexpr bool is_bool() const noexcept { return raw_ == Qtrue || raw_ == Qfalse; }
70
+
71
+ /// @brief Returns whether this value is a Ruby Integer.
72
+ [[nodiscard]] bool is_integer() const noexcept { return RB_INTEGER_TYPE_P(raw_); }
73
+
74
+ /// @brief Returns whether this value is a Ruby Float.
75
+ [[nodiscard]] bool is_float() const noexcept { return RB_TYPE_P(raw_, T_FLOAT); }
76
+
77
+ /// @brief Returns whether this value is a Ruby String.
78
+ [[nodiscard]] bool is_string() const noexcept { return RB_TYPE_P(raw_, T_STRING); }
79
+
80
+ /// @brief Returns whether this value is a Ruby Symbol.
81
+ [[nodiscard]] bool is_symbol() const noexcept { return SYMBOL_P(raw_); }
82
+
83
+ /// @brief Returns whether this value is a Ruby Array.
84
+ [[nodiscard]] bool is_array() const noexcept { return RB_TYPE_P(raw_, T_ARRAY); }
85
+
86
+ /// @brief Returns whether this value is a Ruby Hash.
87
+ [[nodiscard]] bool is_hash() const noexcept { return RB_TYPE_P(raw_, T_HASH); }
88
+
89
+ private:
90
+ VALUE raw_ = Qnil;
91
+ };
92
+
93
+ static_assert(std::is_trivially_copyable_v<value>);
94
+ static_assert(sizeof(value) == sizeof(VALUE));
95
+
96
+ /// @brief Keeps a temporary Ruby value visible to the conservative GC.
97
+ /// @code rbxx::gc_guard(value); @endcode
98
+ inline void gc_guard(value guarded) noexcept {
99
+ VALUE raw = guarded.raw();
100
+ RB_GC_GUARD(raw);
101
+ }
102
+
103
+ } // namespace rbxx
104
+ // END rbxx/value.hpp
105
+
106
+ // BEGIN rbxx/object.hpp
107
+
108
+
109
+ #include <memory>
110
+ #include <utility>
111
+
112
+ namespace rbxx {
113
+ namespace detail {
114
+
115
+ class object_cell {
116
+ public:
117
+ explicit object_cell(VALUE initial) noexcept : stored_(initial) {
118
+ rb_gc_register_address(&stored_);
119
+ }
120
+
121
+ object_cell(const object_cell&) = delete;
122
+ object_cell& operator=(const object_cell&) = delete;
123
+
124
+ ~object_cell() noexcept { rb_gc_unregister_address(&stored_); }
125
+
126
+ [[nodiscard]] VALUE raw() const noexcept { return stored_; }
127
+
128
+ private:
129
+ VALUE stored_;
130
+ };
131
+
132
+ } // namespace detail
133
+
134
+ /// @brief A copyable, movable RAII handle that pins a Ruby object across GC and compaction.
135
+ /// @code rbxx::object pinned{rbxx::value{ruby_value}}; @endcode
136
+ class object {
137
+ public:
138
+ /// @brief Constructs an empty handle representing nil.
139
+ object() noexcept = default;
140
+
141
+ /// @brief Pins the supplied Ruby value.
142
+ /// @code rbxx::object pinned{rbxx::value{Qtrue}}; @endcode
143
+ explicit object(value initial) : cell_(std::make_shared<detail::object_cell>(initial.raw())) {}
144
+
145
+ /// @brief Pins the supplied raw Ruby VALUE.
146
+ explicit object(VALUE initial) : object(value{initial}) {}
147
+
148
+ object(const object&) noexcept = default;
149
+ object(object&&) noexcept = default;
150
+ object& operator=(const object&) noexcept = default;
151
+ object& operator=(object&&) noexcept = default;
152
+ ~object() = default;
153
+
154
+ /// @brief Returns the pinned object as a non-owning value view.
155
+ /// @code rbxx::value current = pinned.get(); @endcode
156
+ [[nodiscard]] value get() const noexcept { return value{raw()}; }
157
+
158
+ /// @brief Returns the current raw VALUE, updated after compaction.
159
+ [[nodiscard]] VALUE raw() const noexcept { return cell_ ? cell_->raw() : Qnil; }
160
+
161
+ /// @brief Returns whether the handle is empty or pins nil.
162
+ [[nodiscard]] bool is_nil() const noexcept { return NIL_P(raw()); }
163
+
164
+ /// @brief Releases this handle's ownership of the pin.
165
+ /// @code pinned.reset(); @endcode
166
+ void reset() noexcept { cell_.reset(); }
167
+
168
+ private:
169
+ std::shared_ptr<detail::object_cell> cell_;
170
+ };
171
+
172
+ } // namespace rbxx
173
+ // END rbxx/object.hpp
174
+
175
+ // BEGIN rbxx/exception.hpp
176
+
177
+
178
+ #include <exception>
179
+ #include <new>
180
+ #include <stdexcept>
181
+ #include <string>
182
+
183
+ namespace rbxx {
184
+
185
+ /// @brief A C++ exception that preserves the original Ruby exception object.
186
+ /// @code catch (const rbxx::ruby_error& error) { error.reraise(); } @endcode
187
+ class ruby_error : public std::exception {
188
+ public:
189
+ /// @brief Pins a Ruby exception for propagation through C++ frames.
190
+ explicit ruby_error(value exception) : exception_(exception) {}
191
+ explicit ruby_error(VALUE exception) : exception_(exception) {}
192
+
193
+ /// @brief Returns a stable description for C++ diagnostics.
194
+ [[nodiscard]] const char* what() const noexcept override { return "Ruby exception"; }
195
+
196
+ /// @brief Returns the preserved Ruby exception.
197
+ [[nodiscard]] value exception() const noexcept { return exception_.get(); }
198
+
199
+ /// @brief Returns the Ruby exception class name.
200
+ [[nodiscard]] std::string ruby_class_name() const;
201
+
202
+ /// @brief Returns the Ruby exception message.
203
+ [[nodiscard]] std::string message() const;
204
+
205
+ /// @brief Throws a copy so the outer Ruby boundary can transparently re-raise it.
206
+ [[noreturn]] void reraise() const { throw *this; }
207
+
208
+ private:
209
+ object exception_;
210
+ };
211
+
212
+ namespace detail {
213
+
214
+ inline VALUE exception_class_name(VALUE exception) { return rb_class_name(CLASS_OF(exception)); }
215
+
216
+ inline VALUE exception_message(VALUE exception) {
217
+ VALUE message = rb_funcall(exception, rb_intern("message"), 0);
218
+ return rb_obj_as_string(message);
219
+ }
220
+
221
+ inline std::string protected_exception_string(VALUE exception, VALUE (*function)(VALUE)) {
222
+ int state = 0;
223
+ VALUE string = rb_protect(function, exception, &state);
224
+ if (state != 0) {
225
+ VALUE nested = rb_errinfo();
226
+ rb_set_errinfo(Qnil);
227
+ throw ruby_error(nested);
228
+ }
229
+ return std::string(RSTRING_PTR(string), static_cast<std::size_t>(RSTRING_LEN(string)));
230
+ }
231
+
232
+ struct exception_creation {
233
+ VALUE klass;
234
+ const char* message;
235
+ };
236
+
237
+ inline VALUE create_exception(VALUE opaque) {
238
+ auto* creation = reinterpret_cast<exception_creation*>(opaque);
239
+ return rb_exc_new_cstr(creation->klass, creation->message);
240
+ }
241
+
242
+ inline VALUE make_exception(VALUE klass, const char* message) noexcept {
243
+ exception_creation creation{klass, message};
244
+ int state = 0;
245
+ VALUE result = rb_protect(create_exception, reinterpret_cast<VALUE>(&creation), &state);
246
+ if (state == 0) {
247
+ return result;
248
+ }
249
+
250
+ result = rb_errinfo();
251
+ rb_set_errinfo(Qnil);
252
+ return result;
253
+ }
254
+
255
+ /// @brief Converts the active C++ exception into a Ruby exception VALUE without raising it.
256
+ /// @code catch (...) { pending = rbxx::detail::translate_current_exception(); } @endcode
257
+ inline VALUE translate_current_exception() noexcept {
258
+ try {
259
+ throw;
260
+ } catch (const ruby_error& error) {
261
+ return error.exception().raw();
262
+ } catch (const std::invalid_argument& error) {
263
+ return make_exception(rb_eArgError, error.what());
264
+ } catch (const std::out_of_range& error) {
265
+ return make_exception(rb_eRangeError, error.what());
266
+ } catch (const std::range_error& error) {
267
+ return make_exception(rb_eRangeError, error.what());
268
+ } catch (const std::bad_alloc& error) {
269
+ return make_exception(rb_eNoMemError, error.what());
270
+ } catch (const std::domain_error& error) {
271
+ return make_exception(rb_eMathDomainError, error.what());
272
+ } catch (const std::exception& error) {
273
+ return make_exception(rb_eRuntimeError, error.what());
274
+ } catch (...) {
275
+ return make_exception(rb_eRuntimeError, "unknown C++ exception");
276
+ }
277
+ }
278
+
279
+ } // namespace detail
280
+
281
+ inline std::string ruby_error::ruby_class_name() const {
282
+ return detail::protected_exception_string(exception_.raw(), detail::exception_class_name);
283
+ }
284
+
285
+ inline std::string ruby_error::message() const {
286
+ return detail::protected_exception_string(exception_.raw(), detail::exception_message);
287
+ }
288
+
289
+ } // namespace rbxx
290
+ // END rbxx/exception.hpp
291
+
292
+ // BEGIN rbxx/protect.hpp
293
+
294
+
295
+ #include <exception>
296
+ #include <functional>
297
+ #include <optional>
298
+ #include <tuple>
299
+ #include <type_traits>
300
+ #include <utility>
301
+
302
+ namespace rbxx {
303
+ namespace detail {
304
+
305
+ template <typename Function, typename... Args> struct protect_payload {
306
+ using function_type = std::decay_t<Function>;
307
+ using result_type = std::invoke_result_t<function_type&, std::decay_t<Args>&...>;
308
+ using stored_result = std::conditional_t<std::is_void_v<result_type>, bool, result_type>;
309
+
310
+ function_type function;
311
+ std::tuple<std::decay_t<Args>...> args;
312
+ std::optional<stored_result> result;
313
+ std::exception_ptr cpp_exception;
314
+ };
315
+
316
+ template <typename Payload> VALUE protect_trampoline(VALUE opaque) noexcept {
317
+ auto* payload = reinterpret_cast<Payload*>(opaque);
318
+ try {
319
+ if constexpr (std::is_void_v<typename Payload::result_type>) {
320
+ std::apply(payload->function, payload->args);
321
+ payload->result.emplace(true);
322
+ } else {
323
+ payload->result.emplace(std::apply(payload->function, payload->args));
324
+ }
325
+ } catch (...) {
326
+ payload->cpp_exception = std::current_exception();
327
+ }
328
+ return Qnil;
329
+ }
330
+
331
+ } // namespace detail
332
+
333
+ /// @brief Invokes a Ruby C API operation through rb_protect and converts longjmp to ruby_error.
334
+ /// @code VALUE string = rbxx::protect(rb_utf8_str_new_cstr, "safe"); @endcode
335
+ template <typename Function, typename... Args> auto protect(Function&& function, Args&&... args) {
336
+ using payload_type = detail::protect_payload<Function, Args...>;
337
+ static_assert(!std::is_reference_v<typename payload_type::result_type>,
338
+ "rbxx::protect does not support reference return types");
339
+
340
+ payload_type payload{std::forward<Function>(function),
341
+ std::tuple<std::decay_t<Args>...>{std::forward<Args>(args)...}, std::nullopt,
342
+ nullptr};
343
+ int state = 0;
344
+ rb_protect(detail::protect_trampoline<payload_type>, reinterpret_cast<VALUE>(&payload), &state);
345
+
346
+ if (state != 0) {
347
+ VALUE exception = rb_errinfo();
348
+ rb_set_errinfo(Qnil);
349
+ throw ruby_error(exception);
350
+ }
351
+ if (payload.cpp_exception) {
352
+ std::rethrow_exception(payload.cpp_exception);
353
+ }
354
+
355
+ if constexpr (std::is_void_v<typename payload_type::result_type>) {
356
+ return;
357
+ } else {
358
+ return std::move(*payload.result);
359
+ }
360
+ }
361
+
362
+ } // namespace rbxx
363
+ // END rbxx/protect.hpp
364
+
365
+ // BEGIN rbxx/type_caster.hpp
366
+
367
+
368
+ #include <concepts>
369
+ #include <cstddef>
370
+ #include <limits>
371
+ #include <string>
372
+ #include <string_view>
373
+ #include <type_traits>
374
+ #include <utility>
375
+
376
+ namespace rbxx {
377
+
378
+ /// @brief Customization point for conversion between Ruby and C++ values.
379
+ /// @code template <> struct rbxx::type_caster<MyType> { /* load/dump/matches */ }; @endcode
380
+ template <typename T, typename Enable = void> struct type_caster;
381
+
382
+ /// @brief True when T can be converted to a Ruby value.
383
+ template <typename T>
384
+ concept to_ruby_convertible = requires(T&& input) {
385
+ {
386
+ type_caster<std::remove_cvref_t<T>>::dump(std::forward<T>(input))
387
+ } -> std::convertible_to<value>;
388
+ };
389
+
390
+ /// @brief True when a Ruby value can be converted to T.
391
+ template <typename T>
392
+ concept from_ruby_convertible =
393
+ requires(value input) { type_caster<std::remove_cvref_t<T>>::load(input); };
394
+
395
+ namespace detail {
396
+
397
+ template <typename T> constexpr std::string_view type_name() noexcept {
398
+ #if defined(_MSC_VER)
399
+ constexpr std::string_view signature = __FUNCSIG__;
400
+ constexpr std::string_view prefix = "type_name<";
401
+ const auto start = signature.find(prefix) + prefix.size();
402
+ return signature.substr(start, signature.find(">(void)", start) - start);
403
+ #else
404
+ constexpr std::string_view signature = __PRETTY_FUNCTION__;
405
+ constexpr std::string_view prefix = "T = ";
406
+ const auto start = signature.find(prefix) + prefix.size();
407
+ const auto end = signature.find_first_of("];", start);
408
+ return signature.substr(start, end - start);
409
+ #endif
410
+ }
411
+
412
+ [[noreturn]] inline void throw_type_error(const char* expected, value actual) {
413
+ const char* actual_name = rb_obj_classname(actual.raw());
414
+ std::string message = "rbxx: expected ";
415
+ message += expected;
416
+ message += ", got ";
417
+ message += actual_name;
418
+ throw ruby_error(make_exception(rb_eTypeError, message.c_str()));
419
+ }
420
+
421
+ template <typename Integer> Integer load_integer(value input) {
422
+ if constexpr (std::is_signed_v<Integer>) {
423
+ const auto converted = protect(rb_num2ll, input.raw());
424
+ constexpr auto minimum = static_cast<LONG_LONG>(std::numeric_limits<Integer>::min());
425
+ constexpr auto maximum = static_cast<LONG_LONG>(std::numeric_limits<Integer>::max());
426
+ if (converted < minimum || converted > maximum) {
427
+ throw std::range_error("rbxx: Integer is outside the target C++ signed integer range");
428
+ }
429
+ return static_cast<Integer>(converted);
430
+ } else {
431
+ const auto converted = protect(rb_num2ull, input.raw());
432
+ constexpr auto maximum = static_cast<unsigned LONG_LONG>(std::numeric_limits<Integer>::max());
433
+ if (converted > maximum) {
434
+ throw std::range_error("rbxx: Integer is outside the target C++ unsigned integer range");
435
+ }
436
+ return static_cast<Integer>(converted);
437
+ }
438
+ }
439
+
440
+ template <typename Integer> value dump_integer(Integer input) {
441
+ if constexpr (std::is_signed_v<Integer>) {
442
+ if (RB_FIXABLE(static_cast<LONG_LONG>(input))) {
443
+ return value{LONG2FIX(static_cast<long>(input))};
444
+ }
445
+ return value{protect(rb_ll2inum, static_cast<LONG_LONG>(input))};
446
+ } else {
447
+ if (RB_POSFIXABLE(static_cast<unsigned LONG_LONG>(input))) {
448
+ return value{LONG2FIX(static_cast<long>(input))};
449
+ }
450
+ return value{protect(rb_ull2inum, static_cast<unsigned LONG_LONG>(input))};
451
+ }
452
+ }
453
+
454
+ inline const char* checked_string_pointer(value input) {
455
+ if (!input.is_string()) {
456
+ throw_type_error("String", input);
457
+ }
458
+ VALUE raw = input.raw();
459
+ return protect([raw]() mutable {
460
+ VALUE string = raw;
461
+ return StringValueCStr(string);
462
+ });
463
+ }
464
+
465
+ inline VALUE checked_string_value(value input) {
466
+ if (input.is_string()) {
467
+ return input.raw();
468
+ }
469
+ VALUE converted = protect(rb_check_string_type, input.raw());
470
+ if (NIL_P(converted)) {
471
+ throw_type_error("String or object responding to #to_str", input);
472
+ }
473
+ return converted;
474
+ }
475
+
476
+ inline value dump_utf8(const char* bytes, std::size_t size) {
477
+ if (size > static_cast<std::size_t>(std::numeric_limits<long>::max())) {
478
+ throw std::length_error("rbxx: String is too large for CRuby");
479
+ }
480
+ return value{protect(rb_utf8_str_new, bytes, static_cast<long>(size))};
481
+ }
482
+
483
+ } // namespace detail
484
+
485
+ template <typename T>
486
+ struct type_caster<T, std::enable_if_t<std::is_integral_v<T> && !std::is_same_v<T, bool>>> {
487
+ static constexpr std::string_view name = "Integer";
488
+
489
+ /// @brief Loads a range-checked C++ integer.
490
+ static T load(value input) { return detail::load_integer<T>(input); }
491
+
492
+ /// @brief Dumps a C++ integer as a Ruby Integer.
493
+ static value dump(T input) { return detail::dump_integer(input); }
494
+
495
+ /// @brief Returns whether the Ruby value is an Integer.
496
+ static bool matches(value input) noexcept { return input.is_integer(); }
497
+ };
498
+
499
+ template <> struct type_caster<bool> {
500
+ static constexpr std::string_view name = "Boolean";
501
+
502
+ static bool load(value input) {
503
+ if (!input.is_bool()) {
504
+ detail::throw_type_error("true or false", input);
505
+ }
506
+ return input.raw() == Qtrue;
507
+ }
508
+
509
+ static value dump(bool input) noexcept { return value{input ? Qtrue : Qfalse}; }
510
+ static bool matches(value input) noexcept { return input.is_bool(); }
511
+ };
512
+
513
+ template <> struct type_caster<float> {
514
+ static constexpr std::string_view name = "Float";
515
+ static float load(value input) {
516
+ if (input.is_float()) {
517
+ return static_cast<float>(RFLOAT_VALUE(input.raw()));
518
+ }
519
+ return static_cast<float>(protect(rb_num2dbl, input.raw()));
520
+ }
521
+ static value dump(float input) {
522
+ return value{protect(rb_float_new, static_cast<double>(input))};
523
+ }
524
+ static bool matches(value input) noexcept { return input.is_float() || input.is_integer(); }
525
+ };
526
+
527
+ template <> struct type_caster<double> {
528
+ static constexpr std::string_view name = "Float";
529
+ static double load(value input) {
530
+ return input.is_float() ? RFLOAT_VALUE(input.raw()) : protect(rb_num2dbl, input.raw());
531
+ }
532
+ static value dump(double input) { return value{protect(rb_float_new, input)}; }
533
+ static bool matches(value input) noexcept { return input.is_float() || input.is_integer(); }
534
+ };
535
+
536
+ template <> struct type_caster<const char*> {
537
+ static constexpr std::string_view name = "String";
538
+ static const char* load(value input) { return detail::checked_string_pointer(input); }
539
+ static value dump(const char* input) {
540
+ if (input == nullptr) {
541
+ detail::throw_type_error("non-null C string", value{Qnil});
542
+ }
543
+ return value{protect(rb_utf8_str_new_cstr, input)};
544
+ }
545
+ static bool matches(value input) noexcept { return input.is_string(); }
546
+ };
547
+
548
+ template <> struct type_caster<std::string> {
549
+ static constexpr std::string_view name = "String";
550
+ static std::string load(value input) {
551
+ VALUE converted = detail::checked_string_value(input);
552
+ const char* bytes = protect([converted]() mutable {
553
+ VALUE string = converted;
554
+ return StringValueCStr(string);
555
+ });
556
+ return std::string(bytes, static_cast<std::size_t>(RSTRING_LEN(converted)));
557
+ }
558
+ static value dump(const std::string& input) {
559
+ return detail::dump_utf8(input.data(), input.size());
560
+ }
561
+ static bool matches(value input) noexcept { return input.is_string(); }
562
+ };
563
+
564
+ template <> struct type_caster<std::string_view> {
565
+ static constexpr std::string_view name = "String";
566
+ static std::string_view load(value input) {
567
+ const char* bytes = detail::checked_string_pointer(input);
568
+ return std::string_view(bytes, static_cast<std::size_t>(RSTRING_LEN(input.raw())));
569
+ }
570
+ static value dump(std::string_view input) {
571
+ return detail::dump_utf8(input.data(), input.size());
572
+ }
573
+ static bool matches(value input) noexcept { return input.is_string(); }
574
+ };
575
+
576
+ template <> struct type_caster<value> {
577
+ static constexpr std::string_view name = "Object";
578
+ static value load(value input) noexcept { return input; }
579
+ static value dump(value input) noexcept { return input; }
580
+ static bool matches(value) noexcept { return true; }
581
+ };
582
+
583
+ template <> struct type_caster<object> {
584
+ static constexpr std::string_view name = "Object";
585
+ static object load(value input) { return object{input}; }
586
+ static value dump(const object& input) noexcept { return input.get(); }
587
+ static bool matches(value) noexcept { return true; }
588
+ };
589
+
590
+ /// @brief Converts a C++ value to Ruby with an actionable missing-caster diagnostic.
591
+ /// @code rbxx::value result = rbxx::to_ruby(42); @endcode
592
+ template <typename T> [[nodiscard]] value to_ruby(T&& input) {
593
+ using converted_type = std::remove_cvref_t<T>;
594
+ static_assert(to_ruby_convertible<converted_type>,
595
+ "rbxx: type has no type_caster; bind it with def_class<T>() or specialize "
596
+ "rbxx::type_caster<T>");
597
+ return type_caster<converted_type>::dump(std::forward<T>(input));
598
+ }
599
+
600
+ /// @brief Converts a Ruby value to C++ with an actionable missing-caster diagnostic.
601
+ /// @code int result = rbxx::from_ruby<int>(ruby_value); @endcode
602
+ template <typename T> [[nodiscard]] decltype(auto) from_ruby(value input) {
603
+ using converted_type = std::remove_cvref_t<T>;
604
+ static_assert(from_ruby_convertible<converted_type>,
605
+ "rbxx: type has no type_caster; bind it with def_class<T>() or specialize "
606
+ "rbxx::type_caster<T>");
607
+ return type_caster<converted_type>::load(input);
608
+ }
609
+
610
+ } // namespace rbxx
611
+ // END rbxx/type_caster.hpp
612
+
613
+ // BEGIN rbxx/arg.hpp
614
+
615
+
616
+ #include <algorithm>
617
+ #include <array>
618
+ #include <cstddef>
619
+ #include <string>
620
+ #include <tuple>
621
+ #include <type_traits>
622
+ #include <utility>
623
+ #include <vector>
624
+
625
+ namespace rbxx {
626
+
627
+ struct argument_spec {
628
+ std::string name;
629
+ bool keyword = false;
630
+ bool has_default = false;
631
+ object default_value;
632
+ };
633
+
634
+ /// @brief Declares a named positional argument and an optional default.
635
+ /// @code function.def("add", fn, rbxx::arg("delta") = 1); @endcode
636
+ class arg {
637
+ public:
638
+ explicit arg(const char* name) : name_(name) {}
639
+ operator argument_spec() const { return argument_spec{name_, false, false, {}}; }
640
+
641
+ template <typename T> argument_spec operator=(T&& default_value) const {
642
+ return argument_spec{name_, false, true, object{to_ruby(std::forward<T>(default_value))}};
643
+ }
644
+
645
+ private:
646
+ std::string name_;
647
+ };
648
+
649
+ /// @brief Declares a keyword argument and an optional default.
650
+ /// @code function.def("open", fn, rbxx::kwarg("mode") = "fast"); @endcode
651
+ class kwarg {
652
+ public:
653
+ explicit kwarg(const char* name) : name_(name) {}
654
+ operator argument_spec() const { return argument_spec{name_, true, false, {}}; }
655
+
656
+ template <typename T> argument_spec operator=(T&& default_value) const {
657
+ return argument_spec{name_, true, true, object{to_ruby(std::forward<T>(default_value))}};
658
+ }
659
+
660
+ private:
661
+ std::string name_;
662
+ };
663
+
664
+ /// @brief Required Ruby block passed to a bound C++ callable.
665
+ class block {
666
+ public:
667
+ explicit block(value callable) : callable_(callable) {}
668
+
669
+ [[nodiscard]] value get() const noexcept { return callable_.get(); }
670
+
671
+ template <typename Return = value, typename... Args> Return call(Args&&... args) const {
672
+ std::array<VALUE, sizeof...(Args)> arguments{to_ruby(std::forward<Args>(args)).raw()...};
673
+ VALUE result = protect(rb_funcallv, callable_.raw(), rb_intern("call"),
674
+ static_cast<int>(sizeof...(Args)), arguments.data());
675
+ if constexpr (std::is_same_v<Return, value>) {
676
+ return value{result};
677
+ } else {
678
+ return from_ruby<Return>(value{result});
679
+ }
680
+ }
681
+
682
+ private:
683
+ object callable_;
684
+ };
685
+
686
+ /// @brief Optional Ruby block passed to a bound C++ callable.
687
+ class optional_block {
688
+ public:
689
+ optional_block() = default;
690
+ explicit optional_block(value callable) : callable_(callable) {}
691
+
692
+ [[nodiscard]] explicit operator bool() const noexcept { return !callable_.is_nil(); }
693
+ [[nodiscard]] value get() const noexcept { return callable_.get(); }
694
+
695
+ template <typename Return = value, typename... Args> Return call(Args&&... args) const {
696
+ if (!*this) {
697
+ throw std::invalid_argument("rbxx: optional block is not present");
698
+ }
699
+ return block{callable_.get()}.template call<Return>(std::forward<Args>(args)...);
700
+ }
701
+
702
+ private:
703
+ object callable_;
704
+ };
705
+
706
+ /// @brief Remaining positional Ruby arguments.
707
+ class args {
708
+ public:
709
+ explicit args(std::vector<value> values) : values_(std::move(values)) {}
710
+ [[nodiscard]] const std::vector<value>& values() const noexcept { return values_; }
711
+ [[nodiscard]] std::size_t size() const noexcept { return values_.size(); }
712
+ [[nodiscard]] value operator[](std::size_t index) const { return values_.at(index); }
713
+
714
+ private:
715
+ std::vector<value> values_;
716
+ };
717
+
718
+ template <> struct type_caster<block> {
719
+ static block load(value input) { return block{input}; }
720
+ static value dump(const block& input) noexcept { return input.get(); }
721
+ static bool matches(value input) noexcept { return RTEST(rb_obj_is_proc(input.raw())); }
722
+ };
723
+
724
+ template <> struct type_caster<optional_block> {
725
+ static optional_block load(value input) {
726
+ return input.is_nil() ? optional_block{} : optional_block{input};
727
+ }
728
+ static value dump(const optional_block& input) noexcept {
729
+ return input ? input.get() : value{Qnil};
730
+ }
731
+ static bool matches(value input) noexcept {
732
+ return input.is_nil() || RTEST(rb_obj_is_proc(input.raw()));
733
+ }
734
+ };
735
+
736
+ template <> struct type_caster<args> {
737
+ static args load(value input) { return args{{input}}; }
738
+ static value dump(const args& input) {
739
+ return value{protect([&input] {
740
+ VALUE result = rb_ary_new_capa(static_cast<long>(input.size()));
741
+ for (value element : input.values()) {
742
+ rb_ary_push(result, element.raw());
743
+ }
744
+ return result;
745
+ })};
746
+ }
747
+ static bool matches(value) noexcept { return true; }
748
+ };
749
+
750
+ namespace detail {
751
+
752
+ template <typename Argument> auto load_argument(value input) {
753
+ using loaded_type = decltype(from_ruby<Argument>(input));
754
+ if constexpr (std::is_lvalue_reference_v<loaded_type>) {
755
+ return std::ref(from_ruby<Argument>(input));
756
+ } else {
757
+ return from_ruby<Argument>(input);
758
+ }
759
+ }
760
+
761
+ template <typename T>
762
+ inline constexpr bool is_block_argument_v = std::is_same_v<std::remove_cvref_t<T>, block>;
763
+ template <typename T>
764
+ inline constexpr bool is_optional_block_argument_v =
765
+ std::is_same_v<std::remove_cvref_t<T>, optional_block>;
766
+ template <typename T>
767
+ inline constexpr bool is_rest_argument_v = std::is_same_v<std::remove_cvref_t<T>, args>;
768
+ template <typename T>
769
+ inline constexpr bool is_special_argument_v =
770
+ is_block_argument_v<T> || is_optional_block_argument_v<T> || is_rest_argument_v<T>;
771
+
772
+ template <typename Tuple, std::size_t... Index>
773
+ consteval std::size_t normal_argument_count(std::index_sequence<Index...>) {
774
+ return (std::size_t{0} + ... +
775
+ (is_special_argument_v<std::tuple_element_t<Index, Tuple>> ? 0U : 1U));
776
+ }
777
+
778
+ template <typename Tuple> consteval std::size_t normal_argument_count() {
779
+ return normal_argument_count<Tuple>(std::make_index_sequence<std::tuple_size_v<Tuple>>{});
780
+ }
781
+
782
+ inline std::vector<argument_spec> make_argument_specs() { return {}; }
783
+
784
+ template <typename... Specs> std::vector<argument_spec> make_argument_specs(Specs&&... specs) {
785
+ static_assert((std::is_convertible_v<Specs, argument_spec> && ...),
786
+ "rbxx: binding options must be arg(...) or kwarg(...)");
787
+ std::vector<argument_spec> result;
788
+ result.reserve(sizeof...(Specs));
789
+ (result.emplace_back(static_cast<argument_spec>(std::forward<Specs>(specs))), ...);
790
+ return result;
791
+ }
792
+
793
+ struct parsed_arguments {
794
+ std::vector<value> slots;
795
+ std::vector<value> rest;
796
+ value block_value{Qnil};
797
+ };
798
+
799
+ template <typename Tuple> class argument_parser {
800
+ public:
801
+ static constexpr std::size_t arity = std::tuple_size_v<Tuple>;
802
+
803
+ explicit argument_parser(std::vector<argument_spec> specs) : specs_(std::move(specs)) {
804
+ constexpr std::size_t normal = normal_argument_count<Tuple>();
805
+ if (specs_.empty()) {
806
+ specs_.reserve(normal);
807
+ for (std::size_t index = 0; index < normal; ++index) {
808
+ specs_.push_back(argument_spec{"arg" + std::to_string(index + 1U), false, false, {}});
809
+ }
810
+ }
811
+ if (specs_.size() != normal) {
812
+ throw std::invalid_argument("rbxx: argument annotations must match non-block parameters");
813
+ }
814
+ }
815
+
816
+ [[nodiscard]] parsed_arguments parse(int argc, const VALUE* argv) const {
817
+ parsed_arguments parsed;
818
+ parsed.slots.resize(arity, value{Qundef});
819
+
820
+ int positional_count = argc;
821
+ VALUE keywords = Qnil;
822
+ if (argc > 0 && rb_keyword_given_p()) {
823
+ keywords = argv[argc - 1];
824
+ --positional_count;
825
+ }
826
+
827
+ std::vector<VALUE> keyword_values(specs_.size(), Qundef);
828
+ read_keywords(keywords, keyword_values);
829
+ fill_slots(parsed, positional_count, argv, keyword_values, std::make_index_sequence<arity>{});
830
+ read_block(parsed);
831
+ return parsed;
832
+ }
833
+
834
+ [[nodiscard]] bool configured() const noexcept {
835
+ return std::any_of(specs_.begin(), specs_.end(),
836
+ [](const argument_spec& spec) { return spec.keyword || spec.has_default; });
837
+ }
838
+
839
+ private:
840
+ void read_keywords(VALUE keywords, std::vector<VALUE>& values) const {
841
+ std::vector<ID> required;
842
+ std::vector<ID> optional;
843
+ std::vector<std::size_t> required_order;
844
+ std::vector<std::size_t> optional_order;
845
+ for (std::size_t index = 0; index < specs_.size(); ++index) {
846
+ const auto& spec = specs_[index];
847
+ if (!spec.keyword) {
848
+ continue;
849
+ }
850
+ ID id = protect(rb_intern, spec.name.c_str());
851
+ (spec.has_default ? optional : required).push_back(id);
852
+ (spec.has_default ? optional_order : required_order).push_back(index);
853
+ }
854
+
855
+ if (required.empty() && optional.empty()) {
856
+ if (!NIL_P(keywords) && RHASH_SIZE(keywords) != 0) {
857
+ throw ruby_error(make_exception(rb_eArgError, "rbxx: unknown keyword arguments"));
858
+ }
859
+ return;
860
+ }
861
+
862
+ if (NIL_P(keywords)) {
863
+ keywords = protect(rb_hash_new);
864
+ }
865
+ std::vector<ID> table = required;
866
+ table.insert(table.end(), optional.begin(), optional.end());
867
+ std::vector<std::size_t> order = required_order;
868
+ order.insert(order.end(), optional_order.begin(), optional_order.end());
869
+ std::vector<VALUE> extracted(table.size(), Qundef);
870
+ protect([&] {
871
+ return rb_get_kwargs(keywords, table.data(), static_cast<int>(required.size()),
872
+ static_cast<int>(optional.size()), extracted.data());
873
+ });
874
+ for (std::size_t index = 0; index < order.size(); ++index) {
875
+ values[order[index]] = extracted[index];
876
+ }
877
+ }
878
+
879
+ template <std::size_t... Index>
880
+ void fill_slots(parsed_arguments& parsed, int positional_count, const VALUE* argv,
881
+ const std::vector<VALUE>& keyword_values, std::index_sequence<Index...>) const {
882
+ std::size_t spec_index = 0;
883
+ int positional_index = 0;
884
+ (fill_slot<Index>(parsed, positional_count, positional_index, argv, spec_index, keyword_values),
885
+ ...);
886
+ if (positional_index < positional_count && !has_rest(std::make_index_sequence<arity>{})) {
887
+ throw ruby_error(make_exception(rb_eArgError, "rbxx: too many positional arguments"));
888
+ }
889
+ }
890
+
891
+ template <std::size_t Index>
892
+ void fill_slot(parsed_arguments& parsed, int positional_count, int& positional_index,
893
+ const VALUE* argv, std::size_t& spec_index,
894
+ const std::vector<VALUE>& keyword_values) const {
895
+ using argument = std::tuple_element_t<Index, Tuple>;
896
+ if constexpr (is_rest_argument_v<argument>) {
897
+ while (positional_index < positional_count) {
898
+ parsed.rest.emplace_back(argv[positional_index++]);
899
+ }
900
+ } else if constexpr (!is_block_argument_v<argument> &&
901
+ !is_optional_block_argument_v<argument>) {
902
+ const argument_spec& spec = specs_[spec_index];
903
+ VALUE selected = Qundef;
904
+ if (spec.keyword) {
905
+ selected = keyword_values[spec_index];
906
+ } else if (positional_index < positional_count) {
907
+ selected = argv[positional_index++];
908
+ }
909
+ if (selected == Qundef && spec.has_default) {
910
+ selected = spec.default_value.raw();
911
+ }
912
+ if (selected == Qundef) {
913
+ std::string message = "rbxx: missing argument ";
914
+ message += spec.name;
915
+ throw ruby_error(make_exception(rb_eArgError, message.c_str()));
916
+ }
917
+ parsed.slots[Index] = value{selected};
918
+ ++spec_index;
919
+ }
920
+ }
921
+
922
+ void read_block(parsed_arguments& parsed) const {
923
+ constexpr bool required = contains_required_block(std::make_index_sequence<arity>{});
924
+ if (rb_block_given_p()) {
925
+ parsed.block_value = value{protect(rb_block_proc)};
926
+ } else if (required) {
927
+ throw ruby_error(make_exception(rb_eArgError, "rbxx: required block was not given"));
928
+ }
929
+ }
930
+
931
+ template <std::size_t... Index>
932
+ static consteval bool contains_required_block(std::index_sequence<Index...>) {
933
+ return (is_block_argument_v<std::tuple_element_t<Index, Tuple>> || ...);
934
+ }
935
+
936
+ template <std::size_t... Index> static consteval bool has_rest(std::index_sequence<Index...>) {
937
+ return (is_rest_argument_v<std::tuple_element_t<Index, Tuple>> || ...);
938
+ }
939
+
940
+ std::vector<argument_spec> specs_;
941
+ };
942
+
943
+ template <typename Argument>
944
+ auto load_parsed_argument(const parsed_arguments& parsed, std::size_t index) {
945
+ if constexpr (is_block_argument_v<Argument>) {
946
+ return block{parsed.block_value};
947
+ } else if constexpr (is_optional_block_argument_v<Argument>) {
948
+ return parsed.block_value.is_nil() ? optional_block{} : optional_block{parsed.block_value};
949
+ } else if constexpr (is_rest_argument_v<Argument>) {
950
+ return args{parsed.rest};
951
+ } else {
952
+ return load_argument<Argument>(parsed.slots[index]);
953
+ }
954
+ }
955
+
956
+ } // namespace detail
957
+
958
+ } // namespace rbxx
959
+ // END rbxx/arg.hpp
960
+
961
+ // BEGIN rbxx/policies.hpp
962
+
963
+ #include <cstddef>
964
+
965
+ namespace rbxx {
966
+
967
+ namespace policy {
968
+
969
+ /// @brief Internal value carried by a public return-value policy constant.
970
+ enum class kind { automatic, copy, take, reference, shared };
971
+
972
+ /// @brief Selects ownership behavior when converting a C++ return value to Ruby.
973
+ struct return_value_policy {
974
+ kind value;
975
+ };
976
+
977
+ /// @name Return-value policies
978
+ /// @{
979
+ inline constexpr return_value_policy automatic{kind::automatic};
980
+ inline constexpr return_value_policy copy{kind::copy};
981
+ inline constexpr return_value_policy take{kind::take};
982
+ inline constexpr return_value_policy reference{kind::reference};
983
+ inline constexpr return_value_policy shared{kind::shared};
984
+ /// @}
985
+
986
+ } // namespace policy
987
+
988
+ /// @brief Keeps one Ruby object alive for as long as another remains reachable.
989
+ /// @details Index 0 is the return value, 1 is self, and 2 onward are arguments.
990
+ /// @code .def("child", &Owner::child, policy::reference, keep_alive<0, 1>()) @endcode
991
+ template <std::size_t Nurse, std::size_t Patient> struct keep_alive {
992
+ static constexpr std::size_t nurse = Nurse;
993
+ static constexpr std::size_t patient = Patient;
994
+ };
995
+
996
+ } // namespace rbxx
997
+ // END rbxx/policies.hpp
998
+
999
+ // BEGIN rbxx/registry.hpp
1000
+
1001
+
1002
+ #include <functional>
1003
+ #include <memory>
1004
+ #include <mutex>
1005
+ #include <source_location>
1006
+ #include <string>
1007
+ #include <typeindex>
1008
+ #include <unordered_map>
1009
+
1010
+ namespace rbxx::detail {
1011
+
1012
+ struct class_info {
1013
+ std::type_index cpp_type{typeid(void)};
1014
+ VALUE ruby_class = Qnil;
1015
+ const rb_data_type_t* data_type = nullptr;
1016
+ std::function<void*(void*)> native_pointer;
1017
+ std::unordered_map<std::type_index, std::function<void*(void*)>> conversions;
1018
+ std::source_location registered_at = std::source_location::current();
1019
+ };
1020
+
1021
+ class type_registry {
1022
+ public:
1023
+ static type_registry& instance() {
1024
+ static auto* registry = new type_registry();
1025
+ return *registry;
1026
+ }
1027
+
1028
+ template <typename T, typename Base = void>
1029
+ void add(VALUE ruby_class, const rb_data_type_t* data_type,
1030
+ std::source_location location = std::source_location::current()) {
1031
+ auto information = std::make_unique<class_info>();
1032
+ information->cpp_type = std::type_index(typeid(T));
1033
+ information->ruby_class = ruby_class;
1034
+ information->data_type = data_type;
1035
+ information->conversions.emplace(std::type_index(typeid(T)),
1036
+ [](void* pointer) { return pointer; });
1037
+ information->registered_at = location;
1038
+
1039
+ if constexpr (!std::is_void_v<Base>) {
1040
+ class_info* base = find(std::type_index(typeid(Base)));
1041
+ if (base == nullptr) {
1042
+ std::string message = "rbxx: base C++ type ";
1043
+ message += type_name<Base>();
1044
+ message += " is not registered; call def_class<Base>() first";
1045
+ throw ruby_error(make_exception(rb_eTypeError, message.c_str()));
1046
+ }
1047
+ for (const auto& [target, conversion] : base->conversions) {
1048
+ information->conversions.emplace(target, [conversion](void* pointer) {
1049
+ auto* derived = static_cast<T*>(pointer);
1050
+ return conversion(static_cast<Base*>(derived));
1051
+ });
1052
+ }
1053
+ }
1054
+
1055
+ std::lock_guard lock(write_mutex_);
1056
+ class_info* stored = information.get();
1057
+ by_data_type_.insert_or_assign(data_type, stored);
1058
+ by_type_.insert_or_assign(std::type_index(typeid(T)), std::move(information));
1059
+ }
1060
+
1061
+ template <typename T> void set_native_pointer(std::function<void*(void*)> extractor) {
1062
+ class_info* information = find(std::type_index(typeid(T)));
1063
+ if (information != nullptr) {
1064
+ information->native_pointer = std::move(extractor);
1065
+ }
1066
+ }
1067
+
1068
+ [[nodiscard]] class_info* find(std::type_index type) const noexcept {
1069
+ auto found = by_type_.find(type);
1070
+ return found == by_type_.end() ? nullptr : found->second.get();
1071
+ }
1072
+
1073
+ [[nodiscard]] class_info* find(const rb_data_type_t* type) const noexcept {
1074
+ auto found = by_data_type_.find(type);
1075
+ return found == by_data_type_.end() ? nullptr : found->second;
1076
+ }
1077
+
1078
+ private:
1079
+ std::mutex write_mutex_;
1080
+ std::unordered_map<std::type_index, std::unique_ptr<class_info>> by_type_;
1081
+ std::unordered_map<const rb_data_type_t*, class_info*> by_data_type_;
1082
+ };
1083
+
1084
+ template <typename T> [[noreturn]] void throw_unregistered_type() {
1085
+ std::string message = "rbxx: C++ type ";
1086
+ message += type_name<T>();
1087
+ message += " is not registered; call def_class<T>() or specialize rbxx::type_caster<T>";
1088
+ throw ruby_error(make_exception(rb_eTypeError, message.c_str()));
1089
+ }
1090
+
1091
+ template <typename T> class_info& registered_class() {
1092
+ class_info* information = type_registry::instance().find(std::type_index(typeid(T)));
1093
+ if (information == nullptr) {
1094
+ throw_unregistered_type<T>();
1095
+ }
1096
+ return *information;
1097
+ }
1098
+
1099
+ template <typename T> T& load_registered(value input) {
1100
+ VALUE raw = input.raw();
1101
+ if (!RB_TYPE_P(raw, T_DATA) || !RTYPEDDATA_P(raw)) {
1102
+ std::string message = "rbxx: expected registered C++ type ";
1103
+ message += type_name<T>();
1104
+ message += ", got ";
1105
+ message += rb_obj_classname(raw);
1106
+ message += "; call def_class<T>() for the expected type";
1107
+ throw ruby_error(make_exception(rb_eTypeError, message.c_str()));
1108
+ }
1109
+
1110
+ class_info* actual = type_registry::instance().find(RTYPEDDATA_TYPE(raw));
1111
+ if (actual == nullptr || !actual->native_pointer) {
1112
+ throw_unregistered_type<T>();
1113
+ }
1114
+ auto conversion = actual->conversions.find(std::type_index(typeid(T)));
1115
+ if (conversion == actual->conversions.end()) {
1116
+ std::string message = "rbxx: wrapped C++ type cannot be converted to ";
1117
+ message += type_name<T>();
1118
+ message += "; register the inheritance with def_class<Derived, Base>()";
1119
+ message += "; actual type was registered at ";
1120
+ message += actual->registered_at.file_name();
1121
+ message += ":";
1122
+ message += std::to_string(actual->registered_at.line());
1123
+ throw ruby_error(make_exception(rb_eTypeError, message.c_str()));
1124
+ }
1125
+
1126
+ void* native = actual->native_pointer(RTYPEDDATA_DATA(raw));
1127
+ if (native == nullptr) {
1128
+ throw ruby_error(make_exception(rb_eRuntimeError, "rbxx: C++ object is not initialized"));
1129
+ }
1130
+ return *static_cast<T*>(conversion->second(native));
1131
+ }
1132
+
1133
+ template <typename T> bool registered_matches(value input) noexcept {
1134
+ if (!RB_TYPE_P(input.raw(), T_DATA) || !RTYPEDDATA_P(input.raw())) {
1135
+ return false;
1136
+ }
1137
+ class_info* actual = type_registry::instance().find(RTYPEDDATA_TYPE(input.raw()));
1138
+ return actual != nullptr &&
1139
+ actual->conversions.contains(std::type_index(typeid(std::remove_cv_t<T>)));
1140
+ }
1141
+
1142
+ } // namespace rbxx::detail
1143
+ // END rbxx/registry.hpp
1144
+
1145
+ // BEGIN rbxx/data_object.hpp
1146
+
1147
+
1148
+ #include <memory>
1149
+ #include <string>
1150
+ #include <type_traits>
1151
+ #include <utility>
1152
+
1153
+ namespace rbxx {
1154
+
1155
+ /// @brief A Ruby VALUE member that remains marked and compaction-safe inside a C++ object.
1156
+ /// @code rbxx::member_value child{ruby_child}; @endcode
1157
+ class member_value {
1158
+ public:
1159
+ member_value() noexcept { rb_gc_register_address(&stored_); }
1160
+ explicit member_value(value initial) noexcept : stored_(initial.raw()) {
1161
+ rb_gc_register_address(&stored_);
1162
+ }
1163
+ explicit member_value(VALUE initial) noexcept : stored_(initial) {
1164
+ rb_gc_register_address(&stored_);
1165
+ }
1166
+ member_value(const member_value& other) noexcept : stored_(other.stored_) {
1167
+ rb_gc_register_address(&stored_);
1168
+ }
1169
+ member_value(member_value&& other) noexcept : stored_(other.stored_) {
1170
+ rb_gc_register_address(&stored_);
1171
+ other.stored_ = Qnil;
1172
+ }
1173
+ member_value& operator=(const member_value& other) noexcept {
1174
+ stored_ = other.stored_;
1175
+ return *this;
1176
+ }
1177
+ member_value& operator=(member_value&& other) noexcept {
1178
+ stored_ = other.stored_;
1179
+ other.stored_ = Qnil;
1180
+ return *this;
1181
+ }
1182
+ ~member_value() noexcept { rb_gc_unregister_address(&stored_); }
1183
+
1184
+ [[nodiscard]] value get() const noexcept { return value{stored_}; }
1185
+ void set(value updated) noexcept { stored_ = updated.raw(); }
1186
+
1187
+ private:
1188
+ VALUE stored_ = Qnil;
1189
+ };
1190
+
1191
+ namespace detail {
1192
+
1193
+ template <typename T> struct is_non_bindable_class : std::false_type {};
1194
+ template <typename T> struct is_non_bindable_class<std::unique_ptr<T>> : std::true_type {};
1195
+ template <typename T> struct is_non_bindable_class<std::shared_ptr<T>> : std::true_type {};
1196
+
1197
+ enum class ownership { owned, borrowed, shared };
1198
+
1199
+ template <typename T> struct data_wrapper {
1200
+ T* pointer = nullptr;
1201
+ ownership mode = ownership::borrowed;
1202
+ std::shared_ptr<T> shared_owner;
1203
+
1204
+ ~data_wrapper() noexcept {
1205
+ if (mode == ownership::owned) {
1206
+ delete pointer;
1207
+ }
1208
+ }
1209
+ };
1210
+
1211
+ template <typename T> void data_mark(void*) noexcept {}
1212
+ template <typename T> void data_compact(void*) noexcept {}
1213
+
1214
+ template <typename T> void data_free(void* raw) noexcept {
1215
+ delete static_cast<data_wrapper<T>*>(raw);
1216
+ }
1217
+
1218
+ template <typename T> std::size_t data_size(const void* raw) noexcept {
1219
+ const auto* wrapper = static_cast<const data_wrapper<T>*>(raw);
1220
+ return sizeof(data_wrapper<T>) +
1221
+ (wrapper != nullptr && wrapper->pointer != nullptr ? sizeof(T) : 0U);
1222
+ }
1223
+
1224
+ template <typename T> rb_data_type_t& typed_data_type() {
1225
+ static auto* name = new std::string("rbxx::" + std::string(type_name<T>()));
1226
+ static rb_data_type_t type = {
1227
+ name->c_str(),
1228
+ {data_mark<T>, data_free<T>, data_size<T>, data_compact<T>, {nullptr}},
1229
+ nullptr,
1230
+ nullptr,
1231
+ RUBY_TYPED_FREE_IMMEDIATELY,
1232
+ };
1233
+ return type;
1234
+ }
1235
+
1236
+ template <typename T> value wrap_data(std::unique_ptr<data_wrapper<T>> wrapper) {
1237
+ class_info& information = registered_class<T>();
1238
+ VALUE result = protect(rb_data_typed_object_wrap, information.ruby_class,
1239
+ static_cast<void*>(wrapper.get()), information.data_type);
1240
+ wrapper.release();
1241
+ return value{result};
1242
+ }
1243
+
1244
+ template <typename T> value wrap_copy(const T& input) {
1245
+ auto wrapper = std::make_unique<data_wrapper<T>>();
1246
+ wrapper->pointer = new T(input);
1247
+ wrapper->mode = ownership::owned;
1248
+ return wrap_data(std::move(wrapper));
1249
+ }
1250
+
1251
+ template <typename T> value wrap_move(T&& input) {
1252
+ auto wrapper = std::make_unique<data_wrapper<T>>();
1253
+ wrapper->pointer = new T(std::move(input));
1254
+ wrapper->mode = ownership::owned;
1255
+ return wrap_data(std::move(wrapper));
1256
+ }
1257
+
1258
+ template <typename T> value wrap_reference(T* input) {
1259
+ if (input == nullptr) {
1260
+ return value{Qnil};
1261
+ }
1262
+ auto wrapper = std::make_unique<data_wrapper<T>>();
1263
+ wrapper->pointer = input;
1264
+ wrapper->mode = ownership::borrowed;
1265
+ return wrap_data(std::move(wrapper));
1266
+ }
1267
+
1268
+ template <typename T> value wrap_take(std::unique_ptr<T> input) {
1269
+ if (!input) {
1270
+ return value{Qnil};
1271
+ }
1272
+ auto wrapper = std::make_unique<data_wrapper<T>>();
1273
+ wrapper->pointer = input.release();
1274
+ wrapper->mode = ownership::owned;
1275
+ return wrap_data(std::move(wrapper));
1276
+ }
1277
+
1278
+ template <typename T> value wrap_shared(std::shared_ptr<T> input) {
1279
+ if (!input) {
1280
+ return value{Qnil};
1281
+ }
1282
+ auto wrapper = std::make_unique<data_wrapper<T>>();
1283
+ wrapper->pointer = input.get();
1284
+ wrapper->mode = ownership::shared;
1285
+ wrapper->shared_owner = std::move(input);
1286
+ return wrap_data(std::move(wrapper));
1287
+ }
1288
+
1289
+ template <typename T, typename Base = void>
1290
+ void register_data_class(VALUE ruby_class,
1291
+ std::source_location location = std::source_location::current()) {
1292
+ rb_data_type_t& type = typed_data_type<T>();
1293
+ if constexpr (!std::is_void_v<Base>) {
1294
+ type.parent = &typed_data_type<Base>();
1295
+ }
1296
+ type_registry::instance().add<T, Base>(ruby_class, &type, location);
1297
+ type_registry::instance().set_native_pointer<T>(
1298
+ [](void* raw) { return static_cast<void*>(static_cast<data_wrapper<T>*>(raw)->pointer); });
1299
+ }
1300
+
1301
+ template <typename T> VALUE allocate_data_object(VALUE klass) {
1302
+ VALUE result = Qnil;
1303
+ VALUE pending = Qnil;
1304
+ try {
1305
+ auto wrapper = std::make_unique<data_wrapper<T>>();
1306
+ result = protect(rb_data_typed_object_wrap, klass, static_cast<void*>(wrapper.get()),
1307
+ &typed_data_type<T>());
1308
+ wrapper.release();
1309
+ } catch (...) {
1310
+ pending = translate_current_exception();
1311
+ }
1312
+ if (!NIL_P(pending)) {
1313
+ rb_exc_raise(pending);
1314
+ }
1315
+ return result;
1316
+ }
1317
+
1318
+ template <typename T> data_wrapper<T>& exact_wrapper(value self) {
1319
+ if (!RB_TYPE_P(self.raw(), T_DATA) || !RTYPEDDATA_P(self.raw()) ||
1320
+ RTYPEDDATA_TYPE(self.raw()) != &typed_data_type<T>()) {
1321
+ throw ruby_error(make_exception(rb_eTypeError, "rbxx: unexpected TypedData class"));
1322
+ }
1323
+ return *static_cast<data_wrapper<T>*>(RTYPEDDATA_DATA(self.raw()));
1324
+ }
1325
+
1326
+ } // namespace detail
1327
+
1328
+ template <typename T>
1329
+ struct type_caster<T, std::enable_if_t<std::is_class_v<T> && !std::is_same_v<T, object> &&
1330
+ !detail::is_non_bindable_class<T>::value>> {
1331
+ static constexpr std::string_view name = "bound C++ object";
1332
+ static T& load(value input) { return detail::load_registered<T>(input); }
1333
+ static value dump(const T& input) { return detail::wrap_copy(input); }
1334
+ static value dump(T&& input) { return detail::wrap_move(std::move(input)); }
1335
+ static bool matches(value input) noexcept { return detail::registered_matches<T>(input); }
1336
+ };
1337
+
1338
+ template <typename T> struct type_caster<T*> {
1339
+ static constexpr std::string_view name = "bound C++ object or nil";
1340
+ static T* load(value input) {
1341
+ return input.is_nil() ? nullptr : std::addressof(detail::load_registered<T>(input));
1342
+ }
1343
+ static value dump(T* input) { return detail::wrap_reference(input); }
1344
+ static bool matches(value input) noexcept {
1345
+ return input.is_nil() || detail::registered_matches<T>(input);
1346
+ }
1347
+ };
1348
+
1349
+ template <typename T> struct type_caster<std::unique_ptr<T>> {
1350
+ static constexpr std::string_view name = "owned C++ object";
1351
+ static value dump(std::unique_ptr<T> input) { return detail::wrap_take(std::move(input)); }
1352
+ static bool matches(value) noexcept { return false; }
1353
+ };
1354
+
1355
+ template <typename T> struct type_caster<std::shared_ptr<T>> {
1356
+ static constexpr std::string_view name = "shared C++ object";
1357
+ static value dump(std::shared_ptr<T> input) { return detail::wrap_shared(std::move(input)); }
1358
+ static bool matches(value input) noexcept {
1359
+ return RB_TYPE_P(input.raw(), T_DATA) && RTYPEDDATA_P(input.raw());
1360
+ }
1361
+ };
1362
+
1363
+ } // namespace rbxx
1364
+ // END rbxx/data_object.hpp
1365
+
1366
+ // BEGIN rbxx/callback.hpp
1367
+
1368
+
1369
+ #include <array>
1370
+ #include <functional>
1371
+ #include <stdexcept>
1372
+ #include <type_traits>
1373
+ #include <utility>
1374
+
1375
+ namespace rbxx {
1376
+
1377
+ namespace detail {
1378
+
1379
+ template <typename Return, typename... Args>
1380
+ struct is_non_bindable_class<std::function<Return(Args...)>> : std::true_type {};
1381
+
1382
+ inline void require_callback_gvl() {
1383
+ #if defined(RBXX_DEBUG)
1384
+ if (ruby_thread_has_gvl_p() == 0) {
1385
+ throw std::runtime_error(
1386
+ "rbxx: Ruby callback invoked without the GVL; reacquire it before calling the Proc");
1387
+ }
1388
+ #endif
1389
+ }
1390
+
1391
+ } // namespace detail
1392
+
1393
+ /// @brief Converts a Ruby Proc into a GC-pinned C++ std::function.
1394
+ template <typename Return, typename... Args> struct type_caster<std::function<Return(Args...)>> {
1395
+ static constexpr std::string_view name = "Proc";
1396
+
1397
+ static std::function<Return(Args...)> load(value input) {
1398
+ if (!matches(input)) {
1399
+ detail::throw_type_error("Proc", input);
1400
+ }
1401
+ object callable{input};
1402
+ return [callable = std::move(callable)](Args... args) -> Return {
1403
+ detail::require_callback_gvl();
1404
+ std::array<VALUE, sizeof...(Args)> converted{to_ruby(std::forward<Args>(args)).raw()...};
1405
+ VALUE result = protect(rb_funcallv, callable.raw(), rb_intern("call"),
1406
+ static_cast<int>(converted.size()), converted.data());
1407
+ if constexpr (std::is_void_v<Return>) {
1408
+ return;
1409
+ } else {
1410
+ return from_ruby<Return>(value{result});
1411
+ }
1412
+ };
1413
+ }
1414
+
1415
+ static bool matches(value input) noexcept { return RTEST(rb_obj_is_proc(input.raw())); }
1416
+ };
1417
+
1418
+ } // namespace rbxx
1419
+ // END rbxx/callback.hpp
1420
+
1421
+ // BEGIN rbxx/function.hpp
1422
+
1423
+
1424
+ #include <functional>
1425
+ #include <limits>
1426
+ #include <memory>
1427
+ #include <mutex>
1428
+ #include <sstream>
1429
+ #include <stdexcept>
1430
+ #include <string>
1431
+ #include <tuple>
1432
+ #include <type_traits>
1433
+ #include <unordered_map>
1434
+ #include <utility>
1435
+ #include <vector>
1436
+
1437
+ namespace rbxx::detail {
1438
+
1439
+ struct keep_alive_spec {
1440
+ std::size_t nurse;
1441
+ std::size_t patient;
1442
+ };
1443
+
1444
+ template <std::size_t Nurse, std::size_t Patient>
1445
+ constexpr keep_alive_spec make_keep_alive_spec(keep_alive<Nurse, Patient>) noexcept {
1446
+ return {Nurse, Patient};
1447
+ }
1448
+
1449
+ inline value keep_alive_value(std::size_t index, value result, value self, int argc,
1450
+ const VALUE* argv) {
1451
+ if (index == 0U) {
1452
+ return result;
1453
+ }
1454
+ if (index == 1U) {
1455
+ return self;
1456
+ }
1457
+ const std::size_t argument = index - 2U;
1458
+ if (argument >= static_cast<std::size_t>(argc)) {
1459
+ throw std::out_of_range("rbxx: keep_alive index exceeds the Ruby argument count");
1460
+ }
1461
+ return value{argv[argument]};
1462
+ }
1463
+
1464
+ inline void apply_keep_alive(const std::vector<keep_alive_spec>& policies, value result, value self,
1465
+ int argc, const VALUE* argv) {
1466
+ for (const keep_alive_spec& selected : policies) {
1467
+ value nurse = keep_alive_value(selected.nurse, result, self, argc, argv);
1468
+ value patient = keep_alive_value(selected.patient, result, self, argc, argv);
1469
+ if (nurse.is_nil() || patient.is_nil()) {
1470
+ continue;
1471
+ }
1472
+ if (selected.nurse > static_cast<std::size_t>(std::numeric_limits<unsigned int>::max())) {
1473
+ throw std::out_of_range("rbxx: keep_alive nurse index is too large");
1474
+ }
1475
+ std::string name = "@__rbxx_keep_" + std::to_string(selected.nurse);
1476
+ protect([nurse, patient, &name] {
1477
+ ID id = rb_intern(name.c_str());
1478
+ VALUE storage = rb_ivar_get(nurse.raw(), id);
1479
+ if (NIL_P(storage)) {
1480
+ storage = rb_ary_new();
1481
+ rb_ivar_set(nurse.raw(), id, storage);
1482
+ } else if (!RB_TYPE_P(storage, T_ARRAY)) {
1483
+ rb_raise(rb_eTypeError, "rbxx: keep_alive storage was replaced with a non-Array");
1484
+ }
1485
+ rb_ary_push(storage, patient.raw());
1486
+ });
1487
+ }
1488
+ }
1489
+
1490
+ template <typename T> struct function_traits;
1491
+
1492
+ template <typename Return, typename... Args> struct function_traits<Return(Args...)> {
1493
+ using return_type = Return;
1494
+ using args_tuple = std::tuple<Args...>;
1495
+ static constexpr std::size_t arity = sizeof...(Args);
1496
+ };
1497
+
1498
+ template <typename Return, typename... Args>
1499
+ struct function_traits<Return (*)(Args...)> : function_traits<Return(Args...)> {};
1500
+
1501
+ template <typename Return, typename... Args>
1502
+ struct function_traits<Return (*)(Args...) noexcept> : function_traits<Return(Args...)> {};
1503
+
1504
+ template <typename Return, typename... Args>
1505
+ struct function_traits<std::function<Return(Args...)>> : function_traits<Return(Args...)> {};
1506
+
1507
+ template <typename Class, typename Return, typename... Args>
1508
+ struct function_traits<Return (Class::*)(Args...)> : function_traits<Return(Args...)> {};
1509
+
1510
+ template <typename Class, typename Return, typename... Args>
1511
+ struct function_traits<Return (Class::*)(Args...) const> : function_traits<Return(Args...)> {};
1512
+
1513
+ template <typename Class, typename Return, typename... Args>
1514
+ struct function_traits<Return (Class::*)(Args...) noexcept> : function_traits<Return(Args...)> {};
1515
+
1516
+ template <typename Class, typename Return, typename... Args>
1517
+ struct function_traits<Return (Class::*)(Args...) const noexcept>
1518
+ : function_traits<Return(Args...)> {};
1519
+
1520
+ template <typename T, typename = void> struct resolved_function_traits {};
1521
+
1522
+ template <typename T>
1523
+ struct resolved_function_traits<T, std::void_t<decltype(&std::remove_reference_t<T>::operator())>>
1524
+ : function_traits<decltype(&std::remove_reference_t<T>::operator())> {};
1525
+
1526
+ template <typename T>
1527
+ requires std::is_function_v<std::remove_pointer_t<std::decay_t<T>>>
1528
+ struct resolved_function_traits<T, void> : function_traits<std::decay_t<T>> {};
1529
+
1530
+ template <typename Return, typename... Args>
1531
+ struct resolved_function_traits<std::function<Return(Args...)>, void>
1532
+ : function_traits<std::function<Return(Args...)>> {};
1533
+
1534
+ template <typename T>
1535
+ concept function_signature = requires {
1536
+ typename resolved_function_traits<std::decay_t<T>>::return_type;
1537
+ typename resolved_function_traits<std::decay_t<T>>::args_tuple;
1538
+ };
1539
+
1540
+ template <typename Argument> int argument_match_score(value input) noexcept {
1541
+ using type = std::remove_cvref_t<Argument>;
1542
+ if constexpr (is_special_argument_v<type>) {
1543
+ return 0;
1544
+ } else if constexpr (std::is_integral_v<type> && !std::is_same_v<type, bool>) {
1545
+ return input.is_integer() ? 2 : 0;
1546
+ } else if constexpr (std::is_floating_point_v<type>) {
1547
+ return input.is_float() ? 2 : (input.is_integer() ? 1 : 0);
1548
+ } else if constexpr (std::is_same_v<type, std::string> ||
1549
+ std::is_same_v<type, std::string_view> ||
1550
+ std::is_same_v<type, const char*>) {
1551
+ return input.is_string() ? 2 : 0;
1552
+ } else if constexpr (std::is_same_v<type, value> || std::is_same_v<type, object>) {
1553
+ return 1;
1554
+ } else {
1555
+ return type_caster<type>::matches(input) ? 2 : 0;
1556
+ }
1557
+ }
1558
+
1559
+ template <typename Tuple, std::size_t... Index>
1560
+ int tuple_match_score(int argc, const VALUE* argv, std::index_sequence<Index...>) noexcept {
1561
+ if (argc != static_cast<int>(sizeof...(Index))) {
1562
+ return -1;
1563
+ }
1564
+ std::array<int, sizeof...(Index)> scores{
1565
+ argument_match_score<std::tuple_element_t<Index, Tuple>>(value{argv[Index]})...};
1566
+ int total = 0;
1567
+ for (int score : scores) {
1568
+ if (score == 0) {
1569
+ return -1;
1570
+ }
1571
+ total += score;
1572
+ }
1573
+ return total;
1574
+ }
1575
+
1576
+ template <typename Tuple, std::size_t... Index>
1577
+ consteval bool arguments_convertible(std::index_sequence<Index...>) {
1578
+ return (from_ruby_convertible<std::tuple_element_t<Index, Tuple>> && ...);
1579
+ }
1580
+
1581
+ template <typename Function> consteval bool function_arguments_convertible() {
1582
+ using traits = resolved_function_traits<std::decay_t<Function>>;
1583
+ using args = typename traits::args_tuple;
1584
+ return arguments_convertible<args>(std::make_index_sequence<std::tuple_size_v<args>>{});
1585
+ }
1586
+
1587
+ template <typename Function> consteval bool function_return_convertible() {
1588
+ using result = typename resolved_function_traits<std::decay_t<Function>>::return_type;
1589
+ return std::is_void_v<result> || to_ruby_convertible<result>;
1590
+ }
1591
+
1592
+ template <typename Result> value dump_result(Result&& result, policy::kind selected);
1593
+
1594
+ class native_function {
1595
+ public:
1596
+ native_function() = default;
1597
+ native_function(const native_function&) = delete;
1598
+ native_function& operator=(const native_function&) = delete;
1599
+ virtual ~native_function() = default;
1600
+
1601
+ [[nodiscard]] virtual value invoke(int argc, const VALUE* argv, value self) = 0;
1602
+ [[nodiscard]] virtual std::string signature() const = 0;
1603
+ [[nodiscard]] virtual bool accepts_arity(int argc) const noexcept = 0;
1604
+ [[nodiscard]] virtual int match_score(int argc, const VALUE* argv) const noexcept = 0;
1605
+ [[nodiscard]] virtual std::size_t declared_arity() const noexcept = 0;
1606
+ };
1607
+
1608
+ template <typename Function> class native_function_impl final : public native_function {
1609
+ public:
1610
+ explicit native_function_impl(Function function, std::vector<argument_spec> specs = {})
1611
+ : function_(std::move(function)), parser_(std::move(specs)) {}
1612
+
1613
+ [[nodiscard]] value invoke(int argc, const VALUE* argv, value) override {
1614
+ using traits = resolved_function_traits<Function>;
1615
+ parsed_arguments parsed = parser_.parse(argc, argv);
1616
+ return invoke_with_args(parsed, std::make_index_sequence<traits::arity>{});
1617
+ }
1618
+
1619
+ [[nodiscard]] std::string signature() const override {
1620
+ return std::string(type_name<Function>());
1621
+ }
1622
+
1623
+ [[nodiscard]] bool accepts_arity(int argc) const noexcept override {
1624
+ using traits = resolved_function_traits<Function>;
1625
+ if (has_rest_args(std::make_index_sequence<traits::arity>{})) {
1626
+ return true;
1627
+ }
1628
+ if (parser_.configured() || has_special_args(std::make_index_sequence<traits::arity>{})) {
1629
+ return argc <= static_cast<int>(traits::arity + 1U);
1630
+ }
1631
+ return argc == static_cast<int>(traits::arity);
1632
+ }
1633
+
1634
+ [[nodiscard]] int match_score(int argc, const VALUE* argv) const noexcept override {
1635
+ using traits = resolved_function_traits<Function>;
1636
+ using args = typename traits::args_tuple;
1637
+ if (parser_.configured() || has_special_args(std::make_index_sequence<traits::arity>{})) {
1638
+ return accepts_arity(argc) ? 0 : -1;
1639
+ }
1640
+ return tuple_match_score<args>(argc, argv, std::make_index_sequence<traits::arity>{});
1641
+ }
1642
+
1643
+ [[nodiscard]] std::size_t declared_arity() const noexcept override {
1644
+ return resolved_function_traits<Function>::arity;
1645
+ }
1646
+
1647
+ private:
1648
+ template <std::size_t... Index>
1649
+ [[nodiscard]] value invoke_with_args(const parsed_arguments& parsed,
1650
+ std::index_sequence<Index...>) {
1651
+ using traits = resolved_function_traits<Function>;
1652
+ using args = typename traits::args_tuple;
1653
+ auto converted = std::tuple<decltype(load_parsed_argument<std::tuple_element_t<Index, args>>(
1654
+ parsed, Index))...>{
1655
+ load_parsed_argument<std::tuple_element_t<Index, args>>(parsed, Index)...};
1656
+
1657
+ if constexpr (std::is_void_v<typename traits::return_type>) {
1658
+ std::apply(function_, converted);
1659
+ return value{Qnil};
1660
+ } else {
1661
+ decltype(auto) result = std::apply(function_, converted);
1662
+ return dump_result(std::forward<decltype(result)>(result), policy::kind::automatic);
1663
+ }
1664
+ }
1665
+
1666
+ template <std::size_t... Index>
1667
+ static consteval bool has_special_args(std::index_sequence<Index...>) {
1668
+ using args = typename resolved_function_traits<Function>::args_tuple;
1669
+ return (is_special_argument_v<std::tuple_element_t<Index, args>> || ...);
1670
+ }
1671
+
1672
+ template <std::size_t... Index>
1673
+ static consteval bool has_rest_args(std::index_sequence<Index...>) {
1674
+ using args = typename resolved_function_traits<Function>::args_tuple;
1675
+ return (is_rest_argument_v<std::tuple_element_t<Index, args>> || ...);
1676
+ }
1677
+
1678
+ Function function_;
1679
+ argument_parser<typename resolved_function_traits<Function>::args_tuple> parser_;
1680
+ };
1681
+
1682
+ template <typename Result> value dump_result(Result&& result, policy::kind selected) {
1683
+ using result_type = Result;
1684
+ using bare_type = std::remove_cvref_t<Result>;
1685
+ if constexpr (std::is_pointer_v<bare_type> && std::is_class_v<std::remove_pointer_t<bare_type>>) {
1686
+ using pointee = std::remove_pointer_t<bare_type>;
1687
+ if (result == nullptr) {
1688
+ return value{Qnil};
1689
+ }
1690
+ if (selected == policy::kind::copy) {
1691
+ return wrap_copy(*result);
1692
+ }
1693
+ if (selected == policy::kind::take) {
1694
+ return wrap_take(std::unique_ptr<pointee>{result});
1695
+ }
1696
+ if (selected == policy::kind::shared) {
1697
+ throw std::invalid_argument("rbxx: shared policy requires std::shared_ptr<T>");
1698
+ }
1699
+ return wrap_reference(result);
1700
+ } else if constexpr (std::is_lvalue_reference_v<result_type> && std::is_class_v<bare_type>) {
1701
+ if (selected == policy::kind::copy) {
1702
+ return wrap_copy(result);
1703
+ }
1704
+ if (selected == policy::kind::take || selected == policy::kind::shared) {
1705
+ throw std::invalid_argument("rbxx: take/shared policy cannot be used with a reference");
1706
+ }
1707
+ return wrap_reference(std::addressof(result));
1708
+ } else {
1709
+ return to_ruby(std::forward<Result>(result));
1710
+ }
1711
+ }
1712
+
1713
+ struct method_key {
1714
+ VALUE owner;
1715
+ ID method;
1716
+
1717
+ friend bool operator==(const method_key&, const method_key&) = default;
1718
+ };
1719
+
1720
+ struct method_key_hash {
1721
+ [[nodiscard]] std::size_t operator()(const method_key& key) const noexcept {
1722
+ const auto owner = static_cast<std::size_t>(key.owner);
1723
+ const auto method = static_cast<std::size_t>(key.method);
1724
+ return owner ^ (method + 0x9e3779b9U + (owner << 6U) + (owner >> 2U));
1725
+ }
1726
+ };
1727
+
1728
+ class method_registry {
1729
+ public:
1730
+ static method_registry& instance() {
1731
+ static auto* registry = new method_registry();
1732
+ return *registry;
1733
+ }
1734
+
1735
+ bool add(VALUE owner, ID method, std::unique_ptr<native_function> function) {
1736
+ std::lock_guard lock(write_mutex_);
1737
+ auto& overloads = functions_[method_key{owner, method}];
1738
+ const bool first = overloads.empty();
1739
+ overloads.push_back(std::move(function));
1740
+ return first;
1741
+ }
1742
+
1743
+ using overloads = std::vector<std::unique_ptr<native_function>>;
1744
+
1745
+ [[nodiscard]] const overloads* find(VALUE self, ID method) const noexcept {
1746
+ if (const auto* direct = find_exact(self, method)) {
1747
+ return direct;
1748
+ }
1749
+
1750
+ VALUE klass = CLASS_OF(self);
1751
+ while (!NIL_P(klass)) {
1752
+ if (const auto* inherited = find_exact(klass, method)) {
1753
+ return inherited;
1754
+ }
1755
+ klass = rb_class_superclass(klass);
1756
+ }
1757
+ return find_exact(Qnil, method);
1758
+ }
1759
+
1760
+ private:
1761
+ [[nodiscard]] const overloads* find_exact(VALUE owner, ID method) const noexcept {
1762
+ auto found = functions_.find(method_key{owner, method});
1763
+ return found == functions_.end() ? nullptr : std::addressof(found->second);
1764
+ }
1765
+
1766
+ std::mutex write_mutex_;
1767
+ std::unordered_map<method_key, overloads, method_key_hash> functions_;
1768
+ };
1769
+
1770
+ inline VALUE function_trampoline(int argc, VALUE* argv, VALUE self) {
1771
+ VALUE pending = Qnil;
1772
+ VALUE result = Qnil;
1773
+ try {
1774
+ ID method = rb_frame_this_func();
1775
+ const auto* functions = method_registry::instance().find(self, method);
1776
+ if (functions == nullptr || functions->empty()) {
1777
+ throw std::runtime_error("rbxx: native function registry entry was not found");
1778
+ }
1779
+ native_function* selected = nullptr;
1780
+ int best_score = -1;
1781
+ for (const auto& candidate : *functions) {
1782
+ int score = candidate->match_score(argc, argv);
1783
+ if (score > best_score) {
1784
+ best_score = score;
1785
+ selected = candidate.get();
1786
+ }
1787
+ }
1788
+ if (selected == nullptr && functions->size() == 1U && functions->front()->accepts_arity(argc)) {
1789
+ selected = functions->front().get();
1790
+ }
1791
+ if (selected == nullptr) {
1792
+ std::string message = "rbxx: no matching overload";
1793
+ if (functions->size() == 1U) {
1794
+ message += "; expected ";
1795
+ message += std::to_string(functions->front()->declared_arity());
1796
+ message += ", actual ";
1797
+ message += std::to_string(argc);
1798
+ }
1799
+ message += "; candidates:";
1800
+ for (const auto& candidate : *functions) {
1801
+ message += "\n ";
1802
+ message += candidate->signature();
1803
+ }
1804
+ throw ruby_error(make_exception(rb_eArgError, message.c_str()));
1805
+ }
1806
+ result = selected->invoke(argc, argv, value{self}).raw();
1807
+ } catch (...) {
1808
+ pending = translate_current_exception();
1809
+ }
1810
+ if (!NIL_P(pending)) {
1811
+ rb_exc_raise(pending);
1812
+ }
1813
+ return result;
1814
+ }
1815
+
1816
+ template <typename Function>
1817
+ void register_function(VALUE owner, const char* name, Function&& function,
1818
+ std::vector<argument_spec> specs = {}, bool global = false) {
1819
+ using stored_function = std::decay_t<Function>;
1820
+ if constexpr (!function_signature<stored_function>) {
1821
+ static_assert(
1822
+ function_signature<stored_function>,
1823
+ "rbxx: callable signature cannot be determined; use a non-generic lambda, function "
1824
+ "pointer, or std::function");
1825
+ } else if constexpr (!function_arguments_convertible<stored_function>()) {
1826
+ static_assert(
1827
+ function_arguments_convertible<stored_function>(),
1828
+ "rbxx: function argument type has no type_caster; bind the type with def_class<T>() "
1829
+ "or specialize rbxx::type_caster<T>");
1830
+ } else if constexpr (!function_return_convertible<stored_function>()) {
1831
+ static_assert(
1832
+ function_return_convertible<stored_function>(),
1833
+ "rbxx: function return type has no type_caster; bind the type with def_class<T>() "
1834
+ "or specialize rbxx::type_caster<T>");
1835
+ } else {
1836
+ ID method = protect(rb_intern, name);
1837
+ const bool first =
1838
+ method_registry::instance().add(global ? Qnil : owner, method,
1839
+ std::make_unique<native_function_impl<stored_function>>(
1840
+ std::forward<Function>(function), std::move(specs)));
1841
+ if (first) {
1842
+ protect([owner, name, global] {
1843
+ if (global) {
1844
+ rb_define_global_function(name, function_trampoline, -1);
1845
+ } else {
1846
+ rb_define_module_function(owner, name, function_trampoline, -1);
1847
+ }
1848
+ });
1849
+ }
1850
+ }
1851
+ }
1852
+
1853
+ template <typename Function>
1854
+ void register_static_function(VALUE owner, const char* name, Function&& function,
1855
+ std::vector<argument_spec> specs = {}) {
1856
+ using stored_function = std::decay_t<Function>;
1857
+ if constexpr (!function_signature<stored_function>) {
1858
+ static_assert(
1859
+ function_signature<stored_function>,
1860
+ "rbxx: callable signature cannot be determined; use a non-generic lambda, function "
1861
+ "pointer, or std::function");
1862
+ } else if constexpr (!function_arguments_convertible<stored_function>()) {
1863
+ static_assert(function_arguments_convertible<stored_function>(),
1864
+ "rbxx: function argument type has no type_caster");
1865
+ } else if constexpr (!function_return_convertible<stored_function>()) {
1866
+ static_assert(function_return_convertible<stored_function>(),
1867
+ "rbxx: function return type has no type_caster");
1868
+ } else {
1869
+ ID method = protect(rb_intern, name);
1870
+ const bool first =
1871
+ method_registry::instance().add(owner, method,
1872
+ std::make_unique<native_function_impl<stored_function>>(
1873
+ std::forward<Function>(function), std::move(specs)));
1874
+ if (first) {
1875
+ protect([owner, name] { rb_define_singleton_method(owner, name, function_trampoline, -1); });
1876
+ }
1877
+ }
1878
+ }
1879
+
1880
+ } // namespace rbxx::detail
1881
+ // END rbxx/function.hpp
1882
+
1883
+ // BEGIN rbxx/operators.hpp
1884
+
1885
+ namespace rbxx::op {
1886
+
1887
+ /// @brief Metadata for mapping a C++ callable to a Ruby operator method.
1888
+ struct name {
1889
+ const char* ruby_name;
1890
+ bool include_comparable = false;
1891
+ };
1892
+
1893
+ /// @name Ruby operator names
1894
+ /// @{
1895
+ inline constexpr name add{"+"};
1896
+ inline constexpr name subtract{"-"};
1897
+ inline constexpr name multiply{"*"};
1898
+ inline constexpr name divide{"/"};
1899
+ inline constexpr name modulo{"%"};
1900
+ inline constexpr name equal{"=="};
1901
+ inline constexpr name compare{"<=>", true};
1902
+ inline constexpr name less{"<"};
1903
+ inline constexpr name greater{">"};
1904
+ inline constexpr name index{"[]"};
1905
+ inline constexpr name index_set{"[]="};
1906
+ inline constexpr name left_shift{"<<"};
1907
+ inline constexpr name to_s{"to_s"};
1908
+ inline constexpr name inspect{"inspect"};
1909
+ inline constexpr name hash{"hash"};
1910
+ inline constexpr name call{"call"};
1911
+ /// @}
1912
+
1913
+ } // namespace rbxx::op
1914
+ // END rbxx/operators.hpp
1915
+
1916
+ // BEGIN rbxx/module.hpp
1917
+
1918
+
1919
+ #include <source_location>
1920
+ #include <utility>
1921
+
1922
+ namespace rbxx {
1923
+
1924
+ template <typename T> class class_;
1925
+
1926
+ /// @brief A non-owning DSL handle for a Ruby Module.
1927
+ /// @code rbxx::define_module("Demo").def("answer", [] { return 42; }); @endcode
1928
+ class module {
1929
+ public:
1930
+ /// @brief Wraps an existing Ruby module VALUE.
1931
+ explicit module(value wrapped) noexcept : wrapped_(wrapped) {}
1932
+
1933
+ /// @brief Returns the wrapped Ruby module.
1934
+ [[nodiscard]] value get() const noexcept { return wrapped_; }
1935
+
1936
+ /// @brief Defines a Ruby module function backed by a C++ callable.
1937
+ /// @code mod.def("sum", [](int a, int b) { return a + b; }); @endcode
1938
+ template <typename Function, typename... Specs>
1939
+ module& def(const char* name, Function&& function, Specs&&... specs) {
1940
+ using traits = detail::resolved_function_traits<std::decay_t<Function>>;
1941
+ if constexpr (detail::function_signature<std::decay_t<Function>>) {
1942
+ static_assert(sizeof...(Specs) == 0U ||
1943
+ sizeof...(Specs) ==
1944
+ detail::normal_argument_count<typename traits::args_tuple>(),
1945
+ "rbxx: argument annotation count must match callable parameters");
1946
+ }
1947
+ detail::register_function(wrapped_.raw(), name, std::forward<Function>(function),
1948
+ detail::make_argument_specs(std::forward<Specs>(specs)...));
1949
+ return *this;
1950
+ }
1951
+
1952
+ template <typename Function, typename... Specs>
1953
+ module& def(op::name operation, Function&& function, Specs&&... specs) {
1954
+ return def(operation.ruby_name, std::forward<Function>(function),
1955
+ std::forward<Specs>(specs)...);
1956
+ }
1957
+
1958
+ /// @brief Defines a Ruby class backed by CRuby TypedData.
1959
+ /// @code auto counter = mod.def_class<Counter>("Counter"); @endcode
1960
+ template <typename T, typename Base = void>
1961
+ class_<T> def_class(const char* name,
1962
+ std::source_location location = std::source_location::current());
1963
+
1964
+ private:
1965
+ value wrapped_;
1966
+ };
1967
+
1968
+ /// @brief Defines or reopens a top-level Ruby module.
1969
+ /// @code rbxx::module demo = rbxx::define_module("Demo"); @endcode
1970
+ [[nodiscard]] inline module define_module(const char* name) {
1971
+ return module{value{protect(rb_define_module, name)}};
1972
+ }
1973
+
1974
+ /// @brief Defines a private Kernel method callable as a Ruby global function.
1975
+ /// @code rbxx::define_global_function("native_sum", [](int a, int b) { return a + b; }); @endcode
1976
+ template <typename Function, typename... Specs>
1977
+ void define_global_function(const char* name, Function&& function, Specs&&... specs) {
1978
+ detail::register_function(Qnil, name, std::forward<Function>(function),
1979
+ detail::make_argument_specs(std::forward<Specs>(specs)...), true);
1980
+ }
1981
+
1982
+ } // namespace rbxx
1983
+ // END rbxx/module.hpp
1984
+
1985
+ // BEGIN rbxx/class.hpp
1986
+
1987
+
1988
+ #include <array>
1989
+ #include <cstddef>
1990
+ #include <iterator>
1991
+ #include <memory>
1992
+ #include <optional>
1993
+ #include <sstream>
1994
+ #include <string>
1995
+ #include <tuple>
1996
+ #include <type_traits>
1997
+ #include <utility>
1998
+
1999
+ namespace rbxx {
2000
+
2001
+ /// @brief Constructor signature marker used by class_::def.
2002
+ /// @code binding.def(rbxx::init<int>()); @endcode
2003
+ template <typename... Args> struct init_tag {};
2004
+
2005
+ /// @brief Creates a constructor signature marker.
2006
+ template <typename... Args> [[nodiscard]] constexpr init_tag<Args...> init() noexcept { return {}; }
2007
+
2008
+ namespace detail {
2009
+
2010
+ template <typename Method> struct member_method_traits;
2011
+
2012
+ template <typename Class, typename Return, typename... Args>
2013
+ struct member_method_traits<Return (Class::*)(Args...)> : function_traits<Return(Args...)> {
2014
+ using class_type = Class;
2015
+ };
2016
+
2017
+ template <typename Class, typename Return, typename... Args>
2018
+ struct member_method_traits<Return (Class::*)(Args...) const> : function_traits<Return(Args...)> {
2019
+ using class_type = Class;
2020
+ };
2021
+
2022
+ template <typename Class, typename Return, typename... Args>
2023
+ struct member_method_traits<Return (Class::*)(Args...) noexcept>
2024
+ : function_traits<Return(Args...)> {
2025
+ using class_type = Class;
2026
+ };
2027
+
2028
+ template <typename Class, typename Return, typename... Args>
2029
+ struct member_method_traits<Return (Class::*)(Args...) const noexcept>
2030
+ : function_traits<Return(Args...)> {
2031
+ using class_type = Class;
2032
+ };
2033
+
2034
+ inline void define_instance_trampoline(VALUE klass, const char* name) {
2035
+ protect([klass, name] { rb_define_method(klass, name, function_trampoline, -1); });
2036
+ }
2037
+
2038
+ struct fast_zero_slot {
2039
+ VALUE (*invoke)(void*, VALUE);
2040
+ void* context;
2041
+ };
2042
+
2043
+ inline constexpr std::size_t fast_zero_slot_count = 64U;
2044
+
2045
+ inline std::array<fast_zero_slot, fast_zero_slot_count>& fast_zero_slots() {
2046
+ static auto* slots = new std::array<fast_zero_slot, fast_zero_slot_count>{};
2047
+ return *slots;
2048
+ }
2049
+
2050
+ template <std::size_t Index> VALUE fast_zero_trampoline(VALUE self) {
2051
+ VALUE result = Qnil;
2052
+ VALUE pending = Qnil;
2053
+ try {
2054
+ const fast_zero_slot& slot = fast_zero_slots()[Index];
2055
+ result = slot.invoke(slot.context, self);
2056
+ } catch (...) {
2057
+ pending = translate_current_exception();
2058
+ }
2059
+ if (!NIL_P(pending)) {
2060
+ rb_exc_raise(pending);
2061
+ }
2062
+ return result;
2063
+ }
2064
+
2065
+ using fast_zero_method = VALUE (*)(VALUE);
2066
+
2067
+ template <std::size_t Index = 0U> fast_zero_method select_fast_zero_method(std::size_t selected) {
2068
+ if constexpr (Index < fast_zero_slot_count) {
2069
+ if (selected == Index) {
2070
+ return &fast_zero_trampoline<Index>;
2071
+ }
2072
+ return select_fast_zero_method<Index + 1U>(selected);
2073
+ }
2074
+ return nullptr;
2075
+ }
2076
+
2077
+ inline std::optional<std::size_t> add_fast_zero_slot(fast_zero_slot slot) {
2078
+ static std::size_t next = 0U;
2079
+ if (next == fast_zero_slot_count) {
2080
+ return std::nullopt;
2081
+ }
2082
+ const std::size_t selected = next++;
2083
+ fast_zero_slots()[selected] = slot;
2084
+ return selected;
2085
+ }
2086
+
2087
+ template <typename Bound> Bound& load_bound_fast(VALUE self) {
2088
+ if (RB_TYPE_P(self, T_DATA) && RTYPEDDATA_P(self) &&
2089
+ RTYPEDDATA_TYPE(self) == &typed_data_type<Bound>()) {
2090
+ auto& wrapper = *static_cast<data_wrapper<Bound>*>(RTYPEDDATA_DATA(self));
2091
+ if (wrapper.pointer == nullptr) {
2092
+ throw ruby_error(make_exception(rb_eRuntimeError, "rbxx: C++ object is not initialized"));
2093
+ }
2094
+ return *wrapper.pointer;
2095
+ }
2096
+ return load_registered<Bound>(value{self});
2097
+ }
2098
+
2099
+ template <typename Bound, typename Method> struct fast_member_context {
2100
+ Method method;
2101
+ policy::kind selected;
2102
+ };
2103
+
2104
+ template <typename Bound, typename Method> VALUE invoke_fast_member(void* opaque, VALUE self) {
2105
+ auto& context = *static_cast<fast_member_context<Bound, Method>*>(opaque);
2106
+ Bound& native = load_bound_fast<Bound>(self);
2107
+ using traits = member_method_traits<Method>;
2108
+ if constexpr (std::is_void_v<typename traits::return_type>) {
2109
+ std::invoke(context.method, native);
2110
+ return Qnil;
2111
+ } else {
2112
+ decltype(auto) result = std::invoke(context.method, native);
2113
+ return dump_result(std::forward<decltype(result)>(result), context.selected).raw();
2114
+ }
2115
+ }
2116
+
2117
+ template <typename Bound, typename Method>
2118
+ bool define_fast_member(VALUE klass, const char* name, Method method, policy::kind selected) {
2119
+ auto context = std::make_unique<fast_member_context<Bound, Method>>(method, selected);
2120
+ const auto slot =
2121
+ add_fast_zero_slot(fast_zero_slot{&invoke_fast_member<Bound, Method>, context.get()});
2122
+ if (!slot) {
2123
+ return false;
2124
+ }
2125
+ fast_zero_method trampoline = select_fast_zero_method(*slot);
2126
+ protect([klass, name, trampoline] { rb_define_method(klass, name, trampoline, 0); });
2127
+ context.release();
2128
+ return true;
2129
+ }
2130
+
2131
+ template <typename Bound, auto Method> VALUE direct_member_trampoline(VALUE self) {
2132
+ VALUE result = Qnil;
2133
+ VALUE pending = Qnil;
2134
+ try {
2135
+ Bound& native = load_bound_fast<Bound>(self);
2136
+ using traits = member_method_traits<decltype(Method)>;
2137
+ if constexpr (std::is_void_v<typename traits::return_type>) {
2138
+ std::invoke(Method, native);
2139
+ } else {
2140
+ decltype(auto) native_result = std::invoke(Method, native);
2141
+ result =
2142
+ dump_result(std::forward<decltype(native_result)>(native_result), policy::kind::automatic)
2143
+ .raw();
2144
+ }
2145
+ } catch (...) {
2146
+ pending = translate_current_exception();
2147
+ }
2148
+ if (!NIL_P(pending)) {
2149
+ rb_exc_raise(pending);
2150
+ }
2151
+ return result;
2152
+ }
2153
+
2154
+ template <typename Bound, auto Method> void define_direct_member(VALUE klass, const char* name) {
2155
+ constexpr fast_zero_method trampoline = &direct_member_trampoline<Bound, Method>;
2156
+ protect([klass, name] { rb_define_method(klass, name, trampoline, 0); });
2157
+ }
2158
+
2159
+ template <typename T, typename... Args> class constructor_function final : public native_function {
2160
+ public:
2161
+ explicit constructor_function(std::vector<argument_spec> specs = {})
2162
+ : parser_(std::move(specs)) {}
2163
+
2164
+ [[nodiscard]] value invoke(int argc, const VALUE* argv, value self) override {
2165
+ parsed_arguments parsed = parser_.parse(argc, argv);
2166
+ auto converted = load(parsed, std::index_sequence_for<Args...>{});
2167
+ auto& wrapper = exact_wrapper<T>(self);
2168
+ if (wrapper.pointer != nullptr) {
2169
+ throw ruby_error(make_exception(rb_eRuntimeError, "rbxx: C++ object is already initialized"));
2170
+ }
2171
+ wrapper.pointer = std::apply(
2172
+ [](auto&&... arguments) { return new T(std::forward<decltype(arguments)>(arguments)...); },
2173
+ converted);
2174
+ wrapper.mode = ownership::owned;
2175
+ return value{Qnil};
2176
+ }
2177
+
2178
+ [[nodiscard]] std::string signature() const override {
2179
+ return "init<" + std::string(type_name<T>()) + ">";
2180
+ }
2181
+
2182
+ [[nodiscard]] bool accepts_arity(int argc) const noexcept override {
2183
+ return parser_.configured() ? argc <= static_cast<int>(sizeof...(Args) + 1U)
2184
+ : argc == static_cast<int>(sizeof...(Args));
2185
+ }
2186
+
2187
+ [[nodiscard]] int match_score(int argc, const VALUE* argv) const noexcept override {
2188
+ if (parser_.configured()) {
2189
+ return accepts_arity(argc) ? 0 : -1;
2190
+ }
2191
+ using tuple = std::tuple<Args...>;
2192
+ return tuple_match_score<tuple>(argc, argv, std::index_sequence_for<Args...>{});
2193
+ }
2194
+
2195
+ [[nodiscard]] std::size_t declared_arity() const noexcept override { return sizeof...(Args); }
2196
+
2197
+ private:
2198
+ template <std::size_t... Index>
2199
+ static auto load(const parsed_arguments& parsed, std::index_sequence<Index...>) {
2200
+ return std::tuple<decltype(load_parsed_argument<Args>(parsed, Index))...>{
2201
+ load_parsed_argument<Args>(parsed, Index)...};
2202
+ }
2203
+
2204
+ argument_parser<std::tuple<Args...>> parser_;
2205
+ };
2206
+
2207
+ template <typename Bound, typename Method> class member_function final : public native_function {
2208
+ public:
2209
+ member_function(Method method, policy::kind selected, std::vector<argument_spec> specs = {},
2210
+ std::vector<keep_alive_spec> keep_alive = {})
2211
+ : method_(method), policy_(selected), parser_(std::move(specs)),
2212
+ keep_alive_(std::move(keep_alive)) {}
2213
+
2214
+ [[nodiscard]] value invoke(int argc, const VALUE* argv, value self) override {
2215
+ using traits = member_method_traits<Method>;
2216
+ parsed_arguments parsed = parser_.parse(argc, argv);
2217
+ Bound& native = load_registered<Bound>(self);
2218
+ value result = invoke_native(native, parsed, std::make_index_sequence<traits::arity>{});
2219
+ apply_keep_alive(keep_alive_, result, self, argc, argv);
2220
+ return result;
2221
+ }
2222
+
2223
+ [[nodiscard]] std::string signature() const override { return std::string(type_name<Method>()); }
2224
+
2225
+ [[nodiscard]] bool accepts_arity(int argc) const noexcept override {
2226
+ constexpr auto arity = member_method_traits<Method>::arity;
2227
+ return parser_.configured() ? argc <= static_cast<int>(arity + 1U)
2228
+ : argc == static_cast<int>(arity);
2229
+ }
2230
+
2231
+ [[nodiscard]] int match_score(int argc, const VALUE* argv) const noexcept override {
2232
+ using traits = member_method_traits<Method>;
2233
+ if (parser_.configured()) {
2234
+ return accepts_arity(argc) ? 0 : -1;
2235
+ }
2236
+ return tuple_match_score<typename traits::args_tuple>(
2237
+ argc, argv, std::make_index_sequence<traits::arity>{});
2238
+ }
2239
+
2240
+ [[nodiscard]] std::size_t declared_arity() const noexcept override {
2241
+ return member_method_traits<Method>::arity;
2242
+ }
2243
+
2244
+ private:
2245
+ template <std::size_t... Index>
2246
+ [[nodiscard]] value invoke_native(Bound& self, const parsed_arguments& parsed,
2247
+ std::index_sequence<Index...>) {
2248
+ using traits = member_method_traits<Method>;
2249
+ using args = typename traits::args_tuple;
2250
+ auto converted = std::tuple<decltype(load_parsed_argument<std::tuple_element_t<Index, args>>(
2251
+ parsed, Index))...>{
2252
+ load_parsed_argument<std::tuple_element_t<Index, args>>(parsed, Index)...};
2253
+ if constexpr (std::is_void_v<typename traits::return_type>) {
2254
+ std::apply([&](auto&&... values) { std::invoke(method_, self, values...); }, converted);
2255
+ return value{Qnil};
2256
+ } else {
2257
+ decltype(auto) result = std::apply(
2258
+ [&](auto&&... values) -> decltype(auto) { return std::invoke(method_, self, values...); },
2259
+ converted);
2260
+ return dump_result(std::forward<decltype(result)>(result), policy_);
2261
+ }
2262
+ }
2263
+
2264
+ Method method_;
2265
+ policy::kind policy_;
2266
+ argument_parser<typename member_method_traits<Method>::args_tuple> parser_;
2267
+ std::vector<keep_alive_spec> keep_alive_;
2268
+ };
2269
+
2270
+ template <typename Tuple, std::size_t... Index>
2271
+ consteval bool self_arguments_convertible(std::index_sequence<Index...>) {
2272
+ return (from_ruby_convertible<std::tuple_element_t<Index + 1U, Tuple>> && ...);
2273
+ }
2274
+
2275
+ template <typename Tuple, std::size_t... Index>
2276
+ auto tuple_tail_type(std::index_sequence<Index...>)
2277
+ -> std::tuple<std::tuple_element_t<Index + 1U, Tuple>...>;
2278
+
2279
+ template <typename Tuple>
2280
+ using tuple_tail_t =
2281
+ decltype(tuple_tail_type<Tuple>(std::make_index_sequence<std::tuple_size_v<Tuple> - 1U>{}));
2282
+
2283
+ template <typename Bound, typename Function> class self_function final : public native_function {
2284
+ public:
2285
+ using traits = resolved_function_traits<Function>;
2286
+ using args = typename traits::args_tuple;
2287
+ static constexpr std::size_t ruby_arity = traits::arity - 1U;
2288
+
2289
+ self_function(Function function, policy::kind selected, std::vector<argument_spec> specs = {},
2290
+ std::vector<keep_alive_spec> keep_alive = {})
2291
+ : function_(std::move(function)), policy_(selected), parser_(std::move(specs)),
2292
+ keep_alive_(std::move(keep_alive)) {
2293
+ static_assert(traits::arity > 0,
2294
+ "rbxx: instance lambda must accept self as its first argument");
2295
+ using self_argument = std::tuple_element_t<0, args>;
2296
+ static_assert(std::is_lvalue_reference_v<self_argument> &&
2297
+ std::is_same_v<std::remove_cvref_t<self_argument>, Bound>,
2298
+ "rbxx: instance lambda first argument must be T& or const T&");
2299
+ static_assert(self_arguments_convertible<args>(std::make_index_sequence<ruby_arity>{}),
2300
+ "rbxx: instance lambda argument type has no type_caster");
2301
+ }
2302
+
2303
+ [[nodiscard]] value invoke(int argc, const VALUE* argv, value self) override {
2304
+ parsed_arguments parsed = parser_.parse(argc, argv);
2305
+ Bound& native = load_registered<Bound>(self);
2306
+ value result = invoke_native(native, parsed, std::make_index_sequence<ruby_arity>{});
2307
+ apply_keep_alive(keep_alive_, result, self, argc, argv);
2308
+ return result;
2309
+ }
2310
+
2311
+ [[nodiscard]] std::string signature() const override {
2312
+ return std::string(type_name<Function>());
2313
+ }
2314
+ [[nodiscard]] bool accepts_arity(int argc) const noexcept override {
2315
+ return parser_.configured() ? argc <= static_cast<int>(ruby_arity + 1U)
2316
+ : argc == static_cast<int>(ruby_arity);
2317
+ }
2318
+ [[nodiscard]] int match_score(int argc, const VALUE* argv) const noexcept override {
2319
+ if (parser_.configured()) {
2320
+ return accepts_arity(argc) ? 0 : -1;
2321
+ }
2322
+ using ruby_args = tuple_tail_t<args>;
2323
+ return tuple_match_score<ruby_args>(argc, argv, std::make_index_sequence<ruby_arity>{});
2324
+ }
2325
+ [[nodiscard]] std::size_t declared_arity() const noexcept override { return ruby_arity; }
2326
+
2327
+ private:
2328
+ template <std::size_t... Index>
2329
+ [[nodiscard]] value invoke_native(Bound& self, const parsed_arguments& parsed,
2330
+ std::index_sequence<Index...>) {
2331
+ auto converted =
2332
+ std::tuple<decltype(load_parsed_argument<std::tuple_element_t<Index + 1U, args>>(
2333
+ parsed, Index))...>{
2334
+ load_parsed_argument<std::tuple_element_t<Index + 1U, args>>(parsed, Index)...};
2335
+ if constexpr (std::is_void_v<typename traits::return_type>) {
2336
+ std::apply([&](auto&&... values) { std::invoke(function_, self, values...); }, converted);
2337
+ return value{Qnil};
2338
+ } else {
2339
+ decltype(auto) result = std::apply(
2340
+ [&](auto&&... values) -> decltype(auto) {
2341
+ return std::invoke(function_, self, values...);
2342
+ },
2343
+ converted);
2344
+ return dump_result(std::forward<decltype(result)>(result), policy_);
2345
+ }
2346
+ }
2347
+
2348
+ Function function_;
2349
+ policy::kind policy_;
2350
+ argument_parser<tuple_tail_t<args>> parser_;
2351
+ std::vector<keep_alive_spec> keep_alive_;
2352
+ };
2353
+
2354
+ template <typename Bound, auto Begin, auto End>
2355
+ class iterable_function final : public native_function {
2356
+ public:
2357
+ [[nodiscard]] value invoke(int, const VALUE*, value self) override {
2358
+ if (rb_block_given_p() == 0) {
2359
+ VALUE enumerator = protect([self] {
2360
+ return rb_enumeratorize_with_size(self.raw(), ID2SYM(rb_intern("each")), 0, nullptr,
2361
+ size_callback);
2362
+ });
2363
+ return value{enumerator};
2364
+ }
2365
+
2366
+ Bound& native = load_registered<Bound>(self);
2367
+ auto iterator = std::invoke(Begin, native);
2368
+ const auto finish = std::invoke(End, native);
2369
+ for (; iterator != finish; ++iterator) {
2370
+ value item = to_ruby(*iterator);
2371
+ protect(rb_yield, item.raw());
2372
+ }
2373
+ return self;
2374
+ }
2375
+
2376
+ [[nodiscard]] std::string signature() const override { return "each()"; }
2377
+ [[nodiscard]] bool accepts_arity(int argc) const noexcept override { return argc == 0; }
2378
+ [[nodiscard]] int match_score(int argc, const VALUE*) const noexcept override {
2379
+ return argc == 0 ? 0 : -1;
2380
+ }
2381
+ [[nodiscard]] std::size_t declared_arity() const noexcept override { return 0U; }
2382
+
2383
+ private:
2384
+ static VALUE size_callback(VALUE self, VALUE, VALUE) {
2385
+ VALUE result = Qnil;
2386
+ VALUE pending = Qnil;
2387
+ try {
2388
+ Bound& native = load_registered<Bound>(value{self});
2389
+ auto first = std::invoke(Begin, native);
2390
+ auto last = std::invoke(End, native);
2391
+ result = to_ruby(std::distance(first, last)).raw();
2392
+ } catch (...) {
2393
+ pending = translate_current_exception();
2394
+ }
2395
+ if (!NIL_P(pending)) {
2396
+ rb_exc_raise(pending);
2397
+ }
2398
+ return result;
2399
+ }
2400
+ };
2401
+
2402
+ } // namespace detail
2403
+
2404
+ /// @brief Fluent binding handle for a C++ class exposed as Ruby TypedData.
2405
+ /// @code binding.def(rbxx::init<int>()).def("value", &Counter::value); @endcode
2406
+ template <typename T> class class_ {
2407
+ public:
2408
+ explicit class_(value ruby_class) noexcept : ruby_class_(ruby_class) {}
2409
+
2410
+ [[nodiscard]] value get() const noexcept { return ruby_class_; }
2411
+
2412
+ /// @brief Defines a compile-time-bound zero-argument member on the minimal dispatch path.
2413
+ /// @code binding.def<&Counter::value>("value"); @endcode
2414
+ template <auto Method> class_& def(const char* name) {
2415
+ using method_type = decltype(Method);
2416
+ static_assert(std::is_member_function_pointer_v<method_type>,
2417
+ "rbxx: compile-time def requires a member function pointer");
2418
+ using traits = detail::member_method_traits<method_type>;
2419
+ static_assert(traits::arity == 0U,
2420
+ "rbxx: compile-time def currently supports zero-argument members");
2421
+ static_assert(std::is_void_v<typename traits::return_type> ||
2422
+ to_ruby_convertible<typename traits::return_type>,
2423
+ "rbxx: member function return type has no type_caster");
2424
+
2425
+ ID method = protect(rb_intern, name);
2426
+ const bool first = detail::method_registry::instance().add(
2427
+ ruby_class_.raw(), method,
2428
+ std::make_unique<detail::member_function<T, method_type>>(Method, policy::kind::automatic));
2429
+ if (first) {
2430
+ detail::define_direct_member<T, Method>(ruby_class_.raw(), name);
2431
+ } else {
2432
+ detail::define_instance_trampoline(ruby_class_.raw(), name);
2433
+ }
2434
+ return *this;
2435
+ }
2436
+
2437
+ /// @brief Defines a C++ constructor and optional Ruby argument annotations.
2438
+ template <typename... Args, typename... Specs>
2439
+ requires((std::is_convertible_v<Specs, argument_spec>) && ...)
2440
+ class_& def(init_tag<Args...>, Specs&&... specs) {
2441
+ static_assert(sizeof...(Specs) == 0U ||
2442
+ sizeof...(Specs) == detail::normal_argument_count<std::tuple<Args...>>(),
2443
+ "rbxx: argument annotation count must match constructor parameters");
2444
+ static_assert((from_ruby_convertible<Args> && ...),
2445
+ "rbxx: constructor argument type has no type_caster");
2446
+ ID method = protect(rb_intern, "initialize");
2447
+ const bool first = detail::method_registry::instance().add(
2448
+ ruby_class_.raw(), method,
2449
+ std::make_unique<detail::constructor_function<T, Args...>>(
2450
+ detail::make_argument_specs(std::forward<Specs>(specs)...)));
2451
+ if (first) {
2452
+ detail::define_instance_trampoline(ruby_class_.raw(), "initialize");
2453
+ }
2454
+ return *this;
2455
+ }
2456
+
2457
+ /// @brief Defines an instance method backed by a member pointer or self-first callable.
2458
+ template <typename Function, typename... Specs>
2459
+ requires((std::is_convertible_v<Specs, argument_spec>) && ...)
2460
+ class_& def(const char* name, Function&& function, Specs&&... specs) {
2461
+ return bind_function(name, std::forward<Function>(function), policy::automatic,
2462
+ detail::make_argument_specs(std::forward<Specs>(specs)...));
2463
+ }
2464
+
2465
+ template <typename Function, typename... Specs>
2466
+ requires((std::is_convertible_v<Specs, argument_spec>) && ...)
2467
+ class_& def(const char* name, Function&& function, policy::return_value_policy selected,
2468
+ Specs&&... specs) {
2469
+ return bind_function(name, std::forward<Function>(function), selected,
2470
+ detail::make_argument_specs(std::forward<Specs>(specs)...));
2471
+ }
2472
+
2473
+ template <typename Function, std::size_t Nurse, std::size_t Patient, typename... Specs>
2474
+ requires((std::is_convertible_v<Specs, argument_spec>) && ...)
2475
+ class_& def(const char* name, Function&& function, policy::return_value_policy selected,
2476
+ keep_alive<Nurse, Patient> lifetime, Specs&&... specs) {
2477
+ return bind_function(name, std::forward<Function>(function), selected,
2478
+ detail::make_argument_specs(std::forward<Specs>(specs)...),
2479
+ {detail::make_keep_alive_spec(lifetime)});
2480
+ }
2481
+
2482
+ template <typename Function, std::size_t Nurse, std::size_t Patient, typename... Specs>
2483
+ requires((std::is_convertible_v<Specs, argument_spec>) && ...)
2484
+ class_& def(const char* name, Function&& function, keep_alive<Nurse, Patient> lifetime,
2485
+ Specs&&... specs) {
2486
+ return bind_function(name, std::forward<Function>(function), policy::automatic,
2487
+ detail::make_argument_specs(std::forward<Specs>(specs)...),
2488
+ {detail::make_keep_alive_spec(lifetime)});
2489
+ }
2490
+
2491
+ template <typename Function, typename... Specs>
2492
+ requires((std::is_convertible_v<Specs, argument_spec>) && ...)
2493
+ class_& def(op::name operation, Function&& function, Specs&&... specs) {
2494
+ class_& result =
2495
+ def(operation.ruby_name, std::forward<Function>(function), std::forward<Specs>(specs)...);
2496
+ include_comparable(operation);
2497
+ return result;
2498
+ }
2499
+
2500
+ template <typename Function, typename... Specs>
2501
+ requires((std::is_convertible_v<Specs, argument_spec>) && ...)
2502
+ class_& def(op::name operation, Function&& function, policy::return_value_policy selected,
2503
+ Specs&&... specs) {
2504
+ class_& result = def(operation.ruby_name, std::forward<Function>(function), selected,
2505
+ std::forward<Specs>(specs)...);
2506
+ include_comparable(operation);
2507
+ return result;
2508
+ }
2509
+
2510
+ /// @brief Defines a singleton method on the bound Ruby class.
2511
+ template <typename Function, typename... Specs>
2512
+ class_& def_static(const char* name, Function&& function, Specs&&... specs) {
2513
+ detail::register_static_function(ruby_class_.raw(), name, std::forward<Function>(function),
2514
+ detail::make_argument_specs(std::forward<Specs>(specs)...));
2515
+ return *this;
2516
+ }
2517
+
2518
+ /// @brief Defines a Ruby reader for a public C++ data member.
2519
+ template <typename Member> class_& def_attr_reader(const char* name, Member T::*member) {
2520
+ return def(name, [member](const T& self) -> const Member& { return self.*member; });
2521
+ }
2522
+
2523
+ /// @brief Defines a Ruby writer for a public C++ data member.
2524
+ template <typename Member> class_& def_attr_writer(const char* name, Member T::*member) {
2525
+ std::string writer = std::string(name) + "=";
2526
+ return def(writer.c_str(), [member](T& self, Member updated) {
2527
+ self.*member = std::move(updated);
2528
+ return self.*member;
2529
+ });
2530
+ }
2531
+
2532
+ /// @brief Defines Ruby reader and writer methods for a public C++ data member.
2533
+ template <typename Member> class_& def_attr_accessor(const char* name, Member T::*member) {
2534
+ def_attr_reader(name, member);
2535
+ return def_attr_writer(name, member);
2536
+ }
2537
+
2538
+ /// @brief Defines Ruby each, includes Enumerable, and returns sized enumerators without a block.
2539
+ template <auto Begin, auto End> class_& def_iterable() {
2540
+ using item_reference = decltype(*std::invoke(Begin, std::declval<T&>()));
2541
+ static_assert(to_ruby_convertible<item_reference>,
2542
+ "rbxx: iterable item type has no type_caster");
2543
+ ID method = protect(rb_intern, "each");
2544
+ const bool first = detail::method_registry::instance().add(
2545
+ ruby_class_.raw(), method, std::make_unique<detail::iterable_function<T, Begin, End>>());
2546
+ if (first) {
2547
+ detail::define_instance_trampoline(ruby_class_.raw(), "each");
2548
+ }
2549
+ protect(rb_include_module, ruby_class_.raw(), rb_mEnumerable);
2550
+ return *this;
2551
+ }
2552
+
2553
+ private:
2554
+ void include_comparable(op::name operation) {
2555
+ if (operation.include_comparable) {
2556
+ protect(rb_include_module, ruby_class_.raw(), rb_mComparable);
2557
+ }
2558
+ }
2559
+
2560
+ template <typename Function>
2561
+ class_& bind_function(const char* name, Function&& function, policy::return_value_policy selected,
2562
+ std::vector<argument_spec> specs,
2563
+ std::vector<detail::keep_alive_spec> keep_alive = {}) {
2564
+ using stored_function = std::decay_t<Function>;
2565
+ ID method = protect(rb_intern, name);
2566
+ if constexpr (std::is_member_function_pointer_v<stored_function>) {
2567
+ using traits = detail::member_method_traits<stored_function>;
2568
+ static_assert(detail::arguments_convertible<typename traits::args_tuple>(
2569
+ std::make_index_sequence<traits::arity>{}),
2570
+ "rbxx: member function argument type has no type_caster");
2571
+ const bool fast = traits::arity == 0U && specs.empty() && keep_alive.empty();
2572
+ const bool first = detail::method_registry::instance().add(
2573
+ ruby_class_.raw(), method,
2574
+ std::make_unique<detail::member_function<T, stored_function>>(
2575
+ function, selected.value, std::move(specs), std::move(keep_alive)));
2576
+ bool defined_fast = false;
2577
+ if constexpr (traits::arity == 0U) {
2578
+ defined_fast =
2579
+ first && fast &&
2580
+ detail::define_fast_member<T>(ruby_class_.raw(), name, function, selected.value);
2581
+ }
2582
+ if (!defined_fast) {
2583
+ detail::define_instance_trampoline(ruby_class_.raw(), name);
2584
+ }
2585
+ } else {
2586
+ static_assert(detail::function_signature<stored_function>,
2587
+ "rbxx: instance callable signature cannot be determined");
2588
+ detail::method_registry::instance().add(
2589
+ ruby_class_.raw(), method,
2590
+ std::make_unique<detail::self_function<T, stored_function>>(
2591
+ std::forward<Function>(function), selected.value, std::move(specs),
2592
+ std::move(keep_alive)));
2593
+ detail::define_instance_trampoline(ruby_class_.raw(), name);
2594
+ }
2595
+ return *this;
2596
+ }
2597
+ value ruby_class_;
2598
+ };
2599
+
2600
+ template <typename T, typename Base>
2601
+ class_<T> module::def_class(const char* name, std::source_location location) {
2602
+ VALUE superclass = rb_cObject;
2603
+ if constexpr (!std::is_void_v<Base>) {
2604
+ static_assert(std::is_base_of_v<Base, T>, "rbxx: Base must be a C++ base class of T");
2605
+ superclass = detail::registered_class<Base>().ruby_class;
2606
+ }
2607
+ VALUE klass = protect(rb_define_class_under, wrapped_.raw(), name, superclass);
2608
+ detail::register_data_class<T, Base>(klass, location);
2609
+ protect([klass] { rb_define_alloc_func(klass, detail::allocate_data_object<T>); });
2610
+ return class_<T>{value{klass}};
2611
+ }
2612
+
2613
+ } // namespace rbxx
2614
+ // END rbxx/class.hpp
2615
+
2616
+ // BEGIN rbxx/extension.hpp
2617
+
2618
+
2619
+ #if defined(_WIN32)
2620
+ #define RBXX_EXPORT __declspec(dllexport)
2621
+ #else
2622
+ #define RBXX_EXPORT __attribute__((visibility("default")))
2623
+ #endif
2624
+
2625
+ #define RBXX_EXTENSION(name) \
2626
+ static void rbxx_init_body_##name(); \
2627
+ extern "C" RBXX_EXPORT void Init_##name() { \
2628
+ VALUE rbxx_pending_exception = Qnil; \
2629
+ try { \
2630
+ rbxx_init_body_##name(); \
2631
+ return; \
2632
+ } catch (...) { \
2633
+ rbxx_pending_exception = ::rbxx::detail::translate_current_exception(); \
2634
+ } \
2635
+ rb_exc_raise(rbxx_pending_exception); \
2636
+ } \
2637
+ static void rbxx_init_body_##name()
2638
+ // END rbxx/extension.hpp
2639
+
2640
+ // BEGIN rbxx/nogvl.hpp
2641
+
2642
+
2643
+ #include <exception>
2644
+ #include <functional>
2645
+ #include <optional>
2646
+ #include <stdexcept>
2647
+ #include <tuple>
2648
+ #include <type_traits>
2649
+ #include <utility>
2650
+
2651
+ namespace rbxx {
2652
+ namespace detail {
2653
+
2654
+ struct default_unblock_function {};
2655
+
2656
+ template <typename T> struct is_std_function : std::false_type {};
2657
+ template <typename Return, typename... Args>
2658
+ struct is_std_function<std::function<Return(Args...)>> : std::true_type {};
2659
+
2660
+ template <typename T>
2661
+ inline constexpr bool gvl_independent_v = !std::is_same_v<std::remove_cvref_t<T>, value> &&
2662
+ !std::is_same_v<std::remove_cvref_t<T>, object> &&
2663
+ !std::is_same_v<std::remove_cvref_t<T>, block> &&
2664
+ !std::is_same_v<std::remove_cvref_t<T>, optional_block> &&
2665
+ !std::is_same_v<std::remove_cvref_t<T>, args> &&
2666
+ !is_std_function<std::remove_cvref_t<T>>::value;
2667
+
2668
+ template <typename Function, typename Unblock, typename Return, typename Tuple>
2669
+ class nogvl_adapter_impl;
2670
+
2671
+ template <typename Function, typename Unblock, typename Return, typename... Args>
2672
+ class nogvl_adapter_impl<Function, Unblock, Return, std::tuple<Args...>> {
2673
+ public:
2674
+ explicit nogvl_adapter_impl(Function function) : function_(std::move(function)) {
2675
+ validate_signature();
2676
+ }
2677
+
2678
+ nogvl_adapter_impl(Function function, Unblock unblock)
2679
+ : function_(std::move(function)), unblock_(std::move(unblock)) {
2680
+ validate_signature();
2681
+ static_assert(std::is_nothrow_invocable_v<Unblock&>,
2682
+ "rbxx: nogvl interrupt function must be noexcept");
2683
+ }
2684
+
2685
+ Return operator()(Args... args) {
2686
+ payload work{this, std::tuple<Args...>{std::forward<Args>(args)...}, std::nullopt, nullptr};
2687
+ if constexpr (std::is_same_v<Unblock, default_unblock_function>) {
2688
+ protect([&work] { rb_thread_call_without_gvl(run, &work, RUBY_UBF_IO, nullptr); });
2689
+ } else {
2690
+ protect([&work, this] {
2691
+ rb_thread_call_without_gvl(run, &work, interrupt, std::addressof(unblock_));
2692
+ });
2693
+ }
2694
+ if (work.exception) {
2695
+ std::rethrow_exception(work.exception);
2696
+ }
2697
+ if constexpr (!std::is_void_v<Return>) {
2698
+ if (!work.result) {
2699
+ throw std::runtime_error("rbxx: nogvl function was interrupted before producing a result");
2700
+ }
2701
+ return std::move(*work.result);
2702
+ }
2703
+ }
2704
+
2705
+ private:
2706
+ using stored_result = std::conditional_t<std::is_void_v<Return>, bool, Return>;
2707
+
2708
+ struct payload {
2709
+ nogvl_adapter_impl* adapter;
2710
+ std::tuple<Args...> arguments;
2711
+ std::optional<stored_result> result;
2712
+ std::exception_ptr exception;
2713
+ };
2714
+
2715
+ static consteval void validate_signature() {
2716
+ static_assert(!std::is_reference_v<Return>,
2717
+ "rbxx: nogvl return values must be owned C++ values");
2718
+ static_assert(gvl_independent_v<Return>,
2719
+ "rbxx: nogvl return type must not access Ruby without the GVL");
2720
+ static_assert((gvl_independent_v<Args> && ...),
2721
+ "rbxx: nogvl arguments must not access Ruby without the GVL");
2722
+ }
2723
+
2724
+ static void* run(void* opaque) noexcept {
2725
+ auto* work = static_cast<payload*>(opaque);
2726
+ try {
2727
+ if constexpr (std::is_void_v<Return>) {
2728
+ std::apply(
2729
+ [&](auto&&... values) {
2730
+ std::invoke(work->adapter->function_, std::forward<Args>(values)...);
2731
+ },
2732
+ work->arguments);
2733
+ work->result.emplace(true);
2734
+ } else {
2735
+ work->result.emplace(std::apply(
2736
+ [&](auto&&... values) -> Return {
2737
+ return std::invoke(work->adapter->function_, std::forward<Args>(values)...);
2738
+ },
2739
+ work->arguments));
2740
+ }
2741
+ } catch (...) {
2742
+ work->exception = std::current_exception();
2743
+ }
2744
+ return nullptr;
2745
+ }
2746
+
2747
+ static void interrupt(void* opaque) noexcept { std::invoke(*static_cast<Unblock*>(opaque)); }
2748
+
2749
+ Function function_;
2750
+ [[no_unique_address]] Unblock unblock_{};
2751
+ };
2752
+
2753
+ template <typename Function, typename Unblock = default_unblock_function>
2754
+ using nogvl_adapter =
2755
+ nogvl_adapter_impl<std::decay_t<Function>, std::decay_t<Unblock>,
2756
+ typename resolved_function_traits<std::decay_t<Function>>::return_type,
2757
+ typename resolved_function_traits<std::decay_t<Function>>::args_tuple>;
2758
+
2759
+ } // namespace detail
2760
+
2761
+ /// @brief Wraps a pure C++ callable so its execution occurs without the Ruby GVL.
2762
+ template <typename Function> [[nodiscard]] auto nogvl(Function&& function) {
2763
+ using stored_function = std::decay_t<Function>;
2764
+ static_assert(detail::function_signature<stored_function>,
2765
+ "rbxx: nogvl requires a callable with a concrete signature");
2766
+ static_assert(!std::is_member_function_pointer_v<stored_function>,
2767
+ "rbxx: nogvl expects a free function or callable object");
2768
+ return detail::nogvl_adapter<stored_function>{std::forward<Function>(function)};
2769
+ }
2770
+
2771
+ /// @brief Wraps a pure C++ callable with a noexcept interruption hook.
2772
+ template <typename Function, typename Unblock>
2773
+ [[nodiscard]] auto nogvl_interruptible(Function&& function, Unblock&& unblock) {
2774
+ using stored_function = std::decay_t<Function>;
2775
+ using stored_unblock = std::decay_t<Unblock>;
2776
+ static_assert(detail::function_signature<stored_function>,
2777
+ "rbxx: nogvl_interruptible requires a callable with a concrete signature");
2778
+ static_assert(!std::is_member_function_pointer_v<stored_function>,
2779
+ "rbxx: nogvl_interruptible expects a free function or callable object");
2780
+ return detail::nogvl_adapter<stored_function, stored_unblock>{std::forward<Function>(function),
2781
+ std::forward<Unblock>(unblock)};
2782
+ }
2783
+
2784
+ } // namespace rbxx
2785
+ // END rbxx/nogvl.hpp
2786
+
2787
+ // BEGIN rbxx/stl/detail.hpp
2788
+
2789
+
2790
+ #include <limits>
2791
+ #include <string>
2792
+ #include <type_traits>
2793
+
2794
+ namespace rbxx::detail {
2795
+
2796
+ inline VALUE coerce_array(value input) {
2797
+ VALUE converted = protect(rb_check_array_type, input.raw());
2798
+ if (NIL_P(converted)) {
2799
+ throw_type_error("Array or object responding to #to_ary", input);
2800
+ }
2801
+ return converted;
2802
+ }
2803
+
2804
+ inline VALUE coerce_hash(value input) {
2805
+ VALUE converted = protect(rb_check_hash_type, input.raw());
2806
+ if (NIL_P(converted)) {
2807
+ throw_type_error("Hash or object responding to #to_hash", input);
2808
+ }
2809
+ return converted;
2810
+ }
2811
+
2812
+ template <typename Range> value dump_array_range(const Range& input) {
2813
+ const auto size = input.size();
2814
+ if (size > static_cast<std::size_t>(std::numeric_limits<long>::max())) {
2815
+ throw std::length_error("rbxx: container is too large for a Ruby Array");
2816
+ }
2817
+ return value{protect([&input, size] {
2818
+ VALUE result = rb_ary_new_capa(static_cast<long>(size));
2819
+ for (const auto& element : input) {
2820
+ using element_type = std::remove_cvref_t<decltype(element)>;
2821
+ if constexpr (std::is_floating_point_v<element_type>) {
2822
+ rb_ary_push(result, rb_float_new(static_cast<double>(element)));
2823
+ } else {
2824
+ rb_ary_push(result, to_ruby(element).raw());
2825
+ }
2826
+ }
2827
+ return result;
2828
+ })};
2829
+ }
2830
+
2831
+ template <typename Map> value dump_hash_range(const Map& input) {
2832
+ return value{protect([&input] {
2833
+ VALUE result = rb_hash_new();
2834
+ for (const auto& [key, mapped] : input) {
2835
+ rb_hash_aset(result, to_ruby(key).raw(), to_ruby(mapped).raw());
2836
+ }
2837
+ return result;
2838
+ })};
2839
+ }
2840
+
2841
+ inline VALUE hash_pairs(VALUE hash) {
2842
+ return protect([hash] { return rb_funcall(hash, rb_intern("to_a"), 0); });
2843
+ }
2844
+
2845
+ } // namespace rbxx::detail
2846
+ // END rbxx/stl/detail.hpp
2847
+
2848
+ // BEGIN rbxx/stl/array.hpp
2849
+
2850
+
2851
+ #include <array>
2852
+ #include <sstream>
2853
+
2854
+ namespace rbxx {
2855
+ namespace detail {
2856
+ template <typename T, std::size_t Size>
2857
+ struct is_non_bindable_class<std::array<T, Size>> : std::true_type {};
2858
+ } // namespace detail
2859
+
2860
+ template <typename T, std::size_t Size> struct type_caster<std::array<T, Size>> {
2861
+ static constexpr std::string_view name = "Array";
2862
+
2863
+ static std::array<T, Size> load(value input) {
2864
+ VALUE array = detail::coerce_array(input);
2865
+ if (RARRAY_LEN(array) != static_cast<long>(Size)) {
2866
+ std::ostringstream message;
2867
+ message << "rbxx: expected Array of length " << Size << ", got " << RARRAY_LEN(array);
2868
+ throw ruby_error(detail::make_exception(rb_eArgError, message.str().c_str()));
2869
+ }
2870
+ return load_elements(array, std::make_index_sequence<Size>{});
2871
+ }
2872
+
2873
+ static value dump(const std::array<T, Size>& input) { return detail::dump_array_range(input); }
2874
+
2875
+ static bool matches(value input) noexcept {
2876
+ return input.is_array() && RARRAY_LEN(input.raw()) == static_cast<long>(Size);
2877
+ }
2878
+
2879
+ private:
2880
+ template <std::size_t... Index>
2881
+ static std::array<T, Size> load_elements(VALUE array, std::index_sequence<Index...>) {
2882
+ return {from_ruby<T>(value{RARRAY_AREF(array, static_cast<long>(Index))})...};
2883
+ }
2884
+ };
2885
+
2886
+ } // namespace rbxx
2887
+ // END rbxx/stl/array.hpp
2888
+
2889
+ // BEGIN rbxx/stl/chrono.hpp
2890
+
2891
+
2892
+ #include <chrono>
2893
+
2894
+ namespace rbxx {
2895
+ namespace detail {
2896
+ template <typename Rep, typename Period>
2897
+ struct is_non_bindable_class<std::chrono::duration<Rep, Period>> : std::true_type {};
2898
+ } // namespace detail
2899
+
2900
+ template <typename Rep, typename Period> struct type_caster<std::chrono::duration<Rep, Period>> {
2901
+ using duration_type = std::chrono::duration<Rep, Period>;
2902
+ static constexpr std::string_view name = "seconds";
2903
+ static duration_type load(value input) {
2904
+ const auto seconds = std::chrono::duration<double>{from_ruby<double>(input)};
2905
+ return std::chrono::duration_cast<duration_type>(seconds);
2906
+ }
2907
+ static value dump(const duration_type& input) {
2908
+ return to_ruby(std::chrono::duration<double>(input).count());
2909
+ }
2910
+ static bool matches(value input) noexcept { return type_caster<double>::matches(input); }
2911
+ };
2912
+
2913
+ } // namespace rbxx
2914
+ // END rbxx/stl/chrono.hpp
2915
+
2916
+ // BEGIN rbxx/stl/filesystem.hpp
2917
+
2918
+
2919
+ #include <filesystem>
2920
+
2921
+ namespace rbxx {
2922
+ namespace detail {
2923
+ template <> struct is_non_bindable_class<std::filesystem::path> : std::true_type {};
2924
+ } // namespace detail
2925
+
2926
+ template <> struct type_caster<std::filesystem::path> {
2927
+ static constexpr std::string_view name = "String path";
2928
+ static std::filesystem::path load(value input) {
2929
+ return std::filesystem::path{from_ruby<std::string>(input)};
2930
+ }
2931
+ static value dump(const std::filesystem::path& input) { return to_ruby(input.string()); }
2932
+ static bool matches(value input) noexcept { return type_caster<std::string>::matches(input); }
2933
+ };
2934
+
2935
+ } // namespace rbxx
2936
+ // END rbxx/stl/filesystem.hpp
2937
+
2938
+ // BEGIN rbxx/stl/map.hpp
2939
+
2940
+
2941
+ #include <map>
2942
+ #include <unordered_map>
2943
+
2944
+ namespace rbxx {
2945
+ namespace detail {
2946
+ template <typename Key, typename Mapped, typename Compare, typename Allocator>
2947
+ struct is_non_bindable_class<std::map<Key, Mapped, Compare, Allocator>> : std::true_type {};
2948
+ template <typename Key, typename Mapped, typename Hash, typename Equal, typename Allocator>
2949
+ struct is_non_bindable_class<std::unordered_map<Key, Mapped, Hash, Equal, Allocator>>
2950
+ : std::true_type {};
2951
+
2952
+ template <typename Map> Map load_map(value input) {
2953
+ VALUE pairs = hash_pairs(coerce_hash(input));
2954
+ Map result;
2955
+ const long length = RARRAY_LEN(pairs);
2956
+ for (long index = 0; index < length; ++index) {
2957
+ VALUE pair = RARRAY_AREF(pairs, index);
2958
+ using key_type = typename Map::key_type;
2959
+ using mapped_type = typename Map::mapped_type;
2960
+ result.emplace(from_ruby<key_type>(value{RARRAY_AREF(pair, 0)}),
2961
+ from_ruby<mapped_type>(value{RARRAY_AREF(pair, 1)}));
2962
+ }
2963
+ return result;
2964
+ }
2965
+ } // namespace detail
2966
+
2967
+ template <typename Key, typename Mapped, typename Compare, typename Allocator>
2968
+ struct type_caster<std::map<Key, Mapped, Compare, Allocator>> {
2969
+ using map_type = std::map<Key, Mapped, Compare, Allocator>;
2970
+ static constexpr std::string_view name = "Hash";
2971
+ static map_type load(value input) { return detail::load_map<map_type>(input); }
2972
+ static value dump(const map_type& input) { return detail::dump_hash_range(input); }
2973
+ static bool matches(value input) noexcept { return input.is_hash(); }
2974
+ };
2975
+
2976
+ template <typename Key, typename Mapped, typename Hash, typename Equal, typename Allocator>
2977
+ struct type_caster<std::unordered_map<Key, Mapped, Hash, Equal, Allocator>> {
2978
+ using map_type = std::unordered_map<Key, Mapped, Hash, Equal, Allocator>;
2979
+ static constexpr std::string_view name = "Hash";
2980
+ static map_type load(value input) { return detail::load_map<map_type>(input); }
2981
+ static value dump(const map_type& input) { return detail::dump_hash_range(input); }
2982
+ static bool matches(value input) noexcept { return input.is_hash(); }
2983
+ };
2984
+
2985
+ } // namespace rbxx
2986
+ // END rbxx/stl/map.hpp
2987
+
2988
+ // BEGIN rbxx/stl/optional.hpp
2989
+
2990
+
2991
+ #include <optional>
2992
+
2993
+ namespace rbxx {
2994
+ namespace detail {
2995
+ template <typename T> struct is_non_bindable_class<std::optional<T>> : std::true_type {};
2996
+ } // namespace detail
2997
+
2998
+ template <typename T> struct type_caster<std::optional<T>> {
2999
+ static constexpr std::string_view name = "Object or nil";
3000
+ static std::optional<T> load(value input) {
3001
+ return input.is_nil() ? std::nullopt : std::optional<T>{from_ruby<T>(input)};
3002
+ }
3003
+ static value dump(const std::optional<T>& input) { return input ? to_ruby(*input) : value{Qnil}; }
3004
+ static bool matches(value input) noexcept {
3005
+ return input.is_nil() || type_caster<T>::matches(input);
3006
+ }
3007
+ };
3008
+
3009
+ } // namespace rbxx
3010
+ // END rbxx/stl/optional.hpp
3011
+
3012
+ // BEGIN rbxx/stl/set.hpp
3013
+
3014
+
3015
+ #include <set>
3016
+
3017
+ namespace rbxx {
3018
+ namespace detail {
3019
+ template <typename T, typename Compare, typename Allocator>
3020
+ struct is_non_bindable_class<std::set<T, Compare, Allocator>> : std::true_type {};
3021
+ } // namespace detail
3022
+
3023
+ template <typename T, typename Compare, typename Allocator>
3024
+ struct type_caster<std::set<T, Compare, Allocator>> {
3025
+ using set_type = std::set<T, Compare, Allocator>;
3026
+ static constexpr std::string_view name = "Array";
3027
+ static set_type load(value input) {
3028
+ VALUE array = detail::coerce_array(input);
3029
+ set_type result;
3030
+ const long length = RARRAY_LEN(array);
3031
+ for (long index = 0; index < length; ++index) {
3032
+ result.insert(from_ruby<T>(value{RARRAY_AREF(array, index)}));
3033
+ }
3034
+ return result;
3035
+ }
3036
+ static value dump(const set_type& input) { return detail::dump_array_range(input); }
3037
+ static bool matches(value input) noexcept { return input.is_array(); }
3038
+ };
3039
+
3040
+ } // namespace rbxx
3041
+ // END rbxx/stl/set.hpp
3042
+
3043
+ // BEGIN rbxx/stl/tuple.hpp
3044
+
3045
+
3046
+ #include <tuple>
3047
+ #include <utility>
3048
+
3049
+ namespace rbxx {
3050
+ namespace detail {
3051
+ template <typename First, typename Second>
3052
+ struct is_non_bindable_class<std::pair<First, Second>> : std::true_type {};
3053
+ template <typename... Items> struct is_non_bindable_class<std::tuple<Items...>> : std::true_type {};
3054
+
3055
+ inline void require_tuple_length(VALUE array, long expected) {
3056
+ if (RARRAY_LEN(array) == expected) {
3057
+ return;
3058
+ }
3059
+ std::string message = "rbxx: expected Array tuple of length ";
3060
+ message += std::to_string(expected);
3061
+ message += ", got ";
3062
+ message += std::to_string(RARRAY_LEN(array));
3063
+ throw ruby_error(make_exception(rb_eArgError, message.c_str()));
3064
+ }
3065
+
3066
+ template <typename Tuple, std::size_t... Index>
3067
+ Tuple load_tuple(VALUE array, std::index_sequence<Index...>) {
3068
+ return Tuple{from_ruby<std::tuple_element_t<Index, Tuple>>(
3069
+ value{RARRAY_AREF(array, static_cast<long>(Index))})...};
3070
+ }
3071
+
3072
+ template <typename Tuple, std::size_t... Index>
3073
+ value dump_tuple(const Tuple& input, std::index_sequence<Index...>) {
3074
+ return value{protect([&input] {
3075
+ VALUE result = rb_ary_new_capa(static_cast<long>(sizeof...(Index)));
3076
+ (rb_ary_push(result, to_ruby(std::get<Index>(input)).raw()), ...);
3077
+ return result;
3078
+ })};
3079
+ }
3080
+ } // namespace detail
3081
+
3082
+ template <typename First, typename Second> struct type_caster<std::pair<First, Second>> {
3083
+ using pair_type = std::pair<First, Second>;
3084
+ static constexpr std::string_view name = "Array(2)";
3085
+ static pair_type load(value input) {
3086
+ VALUE array = detail::coerce_array(input);
3087
+ detail::require_tuple_length(array, 2);
3088
+ return {from_ruby<First>(value{RARRAY_AREF(array, 0)}),
3089
+ from_ruby<Second>(value{RARRAY_AREF(array, 1)})};
3090
+ }
3091
+ static value dump(const pair_type& input) {
3092
+ return detail::dump_tuple(input, std::index_sequence<0, 1>{});
3093
+ }
3094
+ static bool matches(value input) noexcept {
3095
+ return input.is_array() && RARRAY_LEN(input.raw()) == 2;
3096
+ }
3097
+ };
3098
+
3099
+ template <typename... Items> struct type_caster<std::tuple<Items...>> {
3100
+ using tuple_type = std::tuple<Items...>;
3101
+ static constexpr std::string_view name = "Array tuple";
3102
+ static tuple_type load(value input) {
3103
+ VALUE array = detail::coerce_array(input);
3104
+ detail::require_tuple_length(array, static_cast<long>(sizeof...(Items)));
3105
+ return detail::load_tuple<tuple_type>(array, std::index_sequence_for<Items...>{});
3106
+ }
3107
+ static value dump(const tuple_type& input) {
3108
+ return detail::dump_tuple(input, std::index_sequence_for<Items...>{});
3109
+ }
3110
+ static bool matches(value input) noexcept {
3111
+ return input.is_array() && RARRAY_LEN(input.raw()) == static_cast<long>(sizeof...(Items));
3112
+ }
3113
+ };
3114
+
3115
+ } // namespace rbxx
3116
+ // END rbxx/stl/tuple.hpp
3117
+
3118
+ // BEGIN rbxx/stl/variant.hpp
3119
+
3120
+
3121
+ #include <variant>
3122
+
3123
+ namespace rbxx {
3124
+ namespace detail {
3125
+ template <typename... Items>
3126
+ struct is_non_bindable_class<std::variant<Items...>> : std::true_type {};
3127
+
3128
+ template <typename Variant, std::size_t Index = 0> Variant load_variant(value input) {
3129
+ if constexpr (Index == std::variant_size_v<Variant>) {
3130
+ throw_type_error("one of the std::variant alternatives", input);
3131
+ } else {
3132
+ using alternative = std::variant_alternative_t<Index, Variant>;
3133
+ if (type_caster<alternative>::matches(input)) {
3134
+ return Variant{std::in_place_index<Index>, from_ruby<alternative>(input)};
3135
+ }
3136
+ return load_variant<Variant, Index + 1U>(input);
3137
+ }
3138
+ }
3139
+ } // namespace detail
3140
+
3141
+ template <typename... Items> struct type_caster<std::variant<Items...>> {
3142
+ using variant_type = std::variant<Items...>;
3143
+ static constexpr std::string_view name = "variant";
3144
+ static variant_type load(value input) { return detail::load_variant<variant_type>(input); }
3145
+ static value dump(const variant_type& input) {
3146
+ return std::visit([](const auto& selected) { return to_ruby(selected); }, input);
3147
+ }
3148
+ static bool matches(value input) noexcept { return (type_caster<Items>::matches(input) || ...); }
3149
+ };
3150
+
3151
+ } // namespace rbxx
3152
+ // END rbxx/stl/variant.hpp
3153
+
3154
+ // BEGIN rbxx/stl/vector.hpp
3155
+
3156
+
3157
+ #include <vector>
3158
+
3159
+ namespace rbxx {
3160
+ namespace detail {
3161
+ template <typename T, typename Allocator>
3162
+ struct is_non_bindable_class<std::vector<T, Allocator>> : std::true_type {};
3163
+ } // namespace detail
3164
+
3165
+ template <typename T, typename Allocator> struct type_caster<std::vector<T, Allocator>> {
3166
+ static constexpr std::string_view name = "Array";
3167
+
3168
+ static std::vector<T, Allocator> load(value input) {
3169
+ VALUE array = detail::coerce_array(input);
3170
+ const auto length = RARRAY_LEN(array);
3171
+ std::vector<T, Allocator> result;
3172
+ result.reserve(static_cast<std::size_t>(length));
3173
+ for (long index = 0; index < length; ++index) {
3174
+ result.push_back(from_ruby<T>(value{RARRAY_AREF(array, index)}));
3175
+ }
3176
+ return result;
3177
+ }
3178
+
3179
+ static value dump(const std::vector<T, Allocator>& input) {
3180
+ return detail::dump_array_range(input);
3181
+ }
3182
+
3183
+ static bool matches(value input) noexcept { return input.is_array(); }
3184
+ };
3185
+
3186
+ } // namespace rbxx
3187
+ // END rbxx/stl/vector.hpp
3188
+
3189
+ // BEGIN rbxx/stl.hpp
3190
+
3191
+ // END rbxx/stl.hpp
3192
+
3193
+ // BEGIN rbxx/version.hpp
3194
+
3195
+ #define RBXX_VERSION_MAJOR 0
3196
+ #define RBXX_VERSION_MINOR 1
3197
+ #define RBXX_VERSION_PATCH 0
3198
+ #define RBXX_VERSION "0.1.0"
3199
+
3200
+ namespace rbxx {
3201
+
3202
+ /// @brief The rbxx semantic version string.
3203
+ /// @code auto version = rbxx::version; @endcode
3204
+ inline constexpr const char* version = RBXX_VERSION;
3205
+
3206
+ } // namespace rbxx
3207
+ // END rbxx/version.hpp
3208
+
3209
+ // BEGIN rbxx/rbxx.hpp
3210
+
3211
+ // END rbxx/rbxx.hpp