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,463 @@
1
+ #pragma once
2
+
3
+ #include <rbxx/arg.hpp>
4
+ #include <rbxx/data_object.hpp>
5
+ #include <rbxx/policies.hpp>
6
+
7
+ #include <functional>
8
+ #include <limits>
9
+ #include <memory>
10
+ #include <mutex>
11
+ #include <sstream>
12
+ #include <stdexcept>
13
+ #include <string>
14
+ #include <tuple>
15
+ #include <type_traits>
16
+ #include <unordered_map>
17
+ #include <utility>
18
+ #include <vector>
19
+
20
+ namespace rbxx::detail {
21
+
22
+ struct keep_alive_spec {
23
+ std::size_t nurse;
24
+ std::size_t patient;
25
+ };
26
+
27
+ template <std::size_t Nurse, std::size_t Patient>
28
+ constexpr keep_alive_spec make_keep_alive_spec(keep_alive<Nurse, Patient>) noexcept {
29
+ return {Nurse, Patient};
30
+ }
31
+
32
+ inline value keep_alive_value(std::size_t index, value result, value self, int argc,
33
+ const VALUE* argv) {
34
+ if (index == 0U) {
35
+ return result;
36
+ }
37
+ if (index == 1U) {
38
+ return self;
39
+ }
40
+ const std::size_t argument = index - 2U;
41
+ if (argument >= static_cast<std::size_t>(argc)) {
42
+ throw std::out_of_range("rbxx: keep_alive index exceeds the Ruby argument count");
43
+ }
44
+ return value{argv[argument]};
45
+ }
46
+
47
+ inline void apply_keep_alive(const std::vector<keep_alive_spec>& policies, value result, value self,
48
+ int argc, const VALUE* argv) {
49
+ for (const keep_alive_spec& selected : policies) {
50
+ value nurse = keep_alive_value(selected.nurse, result, self, argc, argv);
51
+ value patient = keep_alive_value(selected.patient, result, self, argc, argv);
52
+ if (nurse.is_nil() || patient.is_nil()) {
53
+ continue;
54
+ }
55
+ if (selected.nurse > static_cast<std::size_t>(std::numeric_limits<unsigned int>::max())) {
56
+ throw std::out_of_range("rbxx: keep_alive nurse index is too large");
57
+ }
58
+ std::string name = "@__rbxx_keep_" + std::to_string(selected.nurse);
59
+ protect([nurse, patient, &name] {
60
+ ID id = rb_intern(name.c_str());
61
+ VALUE storage = rb_ivar_get(nurse.raw(), id);
62
+ if (NIL_P(storage)) {
63
+ storage = rb_ary_new();
64
+ rb_ivar_set(nurse.raw(), id, storage);
65
+ } else if (!RB_TYPE_P(storage, T_ARRAY)) {
66
+ rb_raise(rb_eTypeError, "rbxx: keep_alive storage was replaced with a non-Array");
67
+ }
68
+ rb_ary_push(storage, patient.raw());
69
+ });
70
+ }
71
+ }
72
+
73
+ template <typename T> struct function_traits;
74
+
75
+ template <typename Return, typename... Args> struct function_traits<Return(Args...)> {
76
+ using return_type = Return;
77
+ using args_tuple = std::tuple<Args...>;
78
+ static constexpr std::size_t arity = sizeof...(Args);
79
+ };
80
+
81
+ template <typename Return, typename... Args>
82
+ struct function_traits<Return (*)(Args...)> : function_traits<Return(Args...)> {};
83
+
84
+ template <typename Return, typename... Args>
85
+ struct function_traits<Return (*)(Args...) noexcept> : function_traits<Return(Args...)> {};
86
+
87
+ template <typename Return, typename... Args>
88
+ struct function_traits<std::function<Return(Args...)>> : function_traits<Return(Args...)> {};
89
+
90
+ template <typename Class, typename Return, typename... Args>
91
+ struct function_traits<Return (Class::*)(Args...)> : function_traits<Return(Args...)> {};
92
+
93
+ template <typename Class, typename Return, typename... Args>
94
+ struct function_traits<Return (Class::*)(Args...) const> : function_traits<Return(Args...)> {};
95
+
96
+ template <typename Class, typename Return, typename... Args>
97
+ struct function_traits<Return (Class::*)(Args...) noexcept> : function_traits<Return(Args...)> {};
98
+
99
+ template <typename Class, typename Return, typename... Args>
100
+ struct function_traits<Return (Class::*)(Args...) const noexcept>
101
+ : function_traits<Return(Args...)> {};
102
+
103
+ template <typename T, typename = void> struct resolved_function_traits {};
104
+
105
+ template <typename T>
106
+ struct resolved_function_traits<T, std::void_t<decltype(&std::remove_reference_t<T>::operator())>>
107
+ : function_traits<decltype(&std::remove_reference_t<T>::operator())> {};
108
+
109
+ template <typename T>
110
+ requires std::is_function_v<std::remove_pointer_t<std::decay_t<T>>>
111
+ struct resolved_function_traits<T, void> : function_traits<std::decay_t<T>> {};
112
+
113
+ template <typename Return, typename... Args>
114
+ struct resolved_function_traits<std::function<Return(Args...)>, void>
115
+ : function_traits<std::function<Return(Args...)>> {};
116
+
117
+ template <typename T>
118
+ concept function_signature = requires {
119
+ typename resolved_function_traits<std::decay_t<T>>::return_type;
120
+ typename resolved_function_traits<std::decay_t<T>>::args_tuple;
121
+ };
122
+
123
+ template <typename Argument> int argument_match_score(value input) noexcept {
124
+ using type = std::remove_cvref_t<Argument>;
125
+ if constexpr (is_special_argument_v<type>) {
126
+ return 0;
127
+ } else if constexpr (std::is_integral_v<type> && !std::is_same_v<type, bool>) {
128
+ return input.is_integer() ? 2 : 0;
129
+ } else if constexpr (std::is_floating_point_v<type>) {
130
+ return input.is_float() ? 2 : (input.is_integer() ? 1 : 0);
131
+ } else if constexpr (std::is_same_v<type, std::string> ||
132
+ std::is_same_v<type, std::string_view> ||
133
+ std::is_same_v<type, const char*>) {
134
+ return input.is_string() ? 2 : 0;
135
+ } else if constexpr (std::is_same_v<type, value> || std::is_same_v<type, object>) {
136
+ return 1;
137
+ } else {
138
+ return type_caster<type>::matches(input) ? 2 : 0;
139
+ }
140
+ }
141
+
142
+ template <typename Tuple, std::size_t... Index>
143
+ int tuple_match_score(int argc, const VALUE* argv, std::index_sequence<Index...>) noexcept {
144
+ if (argc != static_cast<int>(sizeof...(Index))) {
145
+ return -1;
146
+ }
147
+ std::array<int, sizeof...(Index)> scores{
148
+ argument_match_score<std::tuple_element_t<Index, Tuple>>(value{argv[Index]})...};
149
+ int total = 0;
150
+ for (int score : scores) {
151
+ if (score == 0) {
152
+ return -1;
153
+ }
154
+ total += score;
155
+ }
156
+ return total;
157
+ }
158
+
159
+ template <typename Tuple, std::size_t... Index>
160
+ consteval bool arguments_convertible(std::index_sequence<Index...>) {
161
+ return (from_ruby_convertible<std::tuple_element_t<Index, Tuple>> && ...);
162
+ }
163
+
164
+ template <typename Function> consteval bool function_arguments_convertible() {
165
+ using traits = resolved_function_traits<std::decay_t<Function>>;
166
+ using args = typename traits::args_tuple;
167
+ return arguments_convertible<args>(std::make_index_sequence<std::tuple_size_v<args>>{});
168
+ }
169
+
170
+ template <typename Function> consteval bool function_return_convertible() {
171
+ using result = typename resolved_function_traits<std::decay_t<Function>>::return_type;
172
+ return std::is_void_v<result> || to_ruby_convertible<result>;
173
+ }
174
+
175
+ template <typename Result> value dump_result(Result&& result, policy::kind selected);
176
+
177
+ class native_function {
178
+ public:
179
+ native_function() = default;
180
+ native_function(const native_function&) = delete;
181
+ native_function& operator=(const native_function&) = delete;
182
+ virtual ~native_function() = default;
183
+
184
+ [[nodiscard]] virtual value invoke(int argc, const VALUE* argv, value self) = 0;
185
+ [[nodiscard]] virtual std::string signature() const = 0;
186
+ [[nodiscard]] virtual bool accepts_arity(int argc) const noexcept = 0;
187
+ [[nodiscard]] virtual int match_score(int argc, const VALUE* argv) const noexcept = 0;
188
+ [[nodiscard]] virtual std::size_t declared_arity() const noexcept = 0;
189
+ };
190
+
191
+ template <typename Function> class native_function_impl final : public native_function {
192
+ public:
193
+ explicit native_function_impl(Function function, std::vector<argument_spec> specs = {})
194
+ : function_(std::move(function)), parser_(std::move(specs)) {}
195
+
196
+ [[nodiscard]] value invoke(int argc, const VALUE* argv, value) override {
197
+ using traits = resolved_function_traits<Function>;
198
+ parsed_arguments parsed = parser_.parse(argc, argv);
199
+ return invoke_with_args(parsed, std::make_index_sequence<traits::arity>{});
200
+ }
201
+
202
+ [[nodiscard]] std::string signature() const override {
203
+ return std::string(type_name<Function>());
204
+ }
205
+
206
+ [[nodiscard]] bool accepts_arity(int argc) const noexcept override {
207
+ using traits = resolved_function_traits<Function>;
208
+ if (has_rest_args(std::make_index_sequence<traits::arity>{})) {
209
+ return true;
210
+ }
211
+ if (parser_.configured() || has_special_args(std::make_index_sequence<traits::arity>{})) {
212
+ return argc <= static_cast<int>(traits::arity + 1U);
213
+ }
214
+ return argc == static_cast<int>(traits::arity);
215
+ }
216
+
217
+ [[nodiscard]] int match_score(int argc, const VALUE* argv) const noexcept override {
218
+ using traits = resolved_function_traits<Function>;
219
+ using args = typename traits::args_tuple;
220
+ if (parser_.configured() || has_special_args(std::make_index_sequence<traits::arity>{})) {
221
+ return accepts_arity(argc) ? 0 : -1;
222
+ }
223
+ return tuple_match_score<args>(argc, argv, std::make_index_sequence<traits::arity>{});
224
+ }
225
+
226
+ [[nodiscard]] std::size_t declared_arity() const noexcept override {
227
+ return resolved_function_traits<Function>::arity;
228
+ }
229
+
230
+ private:
231
+ template <std::size_t... Index>
232
+ [[nodiscard]] value invoke_with_args(const parsed_arguments& parsed,
233
+ std::index_sequence<Index...>) {
234
+ using traits = resolved_function_traits<Function>;
235
+ using args = typename traits::args_tuple;
236
+ auto converted = std::tuple<decltype(load_parsed_argument<std::tuple_element_t<Index, args>>(
237
+ parsed, Index))...>{
238
+ load_parsed_argument<std::tuple_element_t<Index, args>>(parsed, Index)...};
239
+
240
+ if constexpr (std::is_void_v<typename traits::return_type>) {
241
+ std::apply(function_, converted);
242
+ return value{Qnil};
243
+ } else {
244
+ decltype(auto) result = std::apply(function_, converted);
245
+ return dump_result(std::forward<decltype(result)>(result), policy::kind::automatic);
246
+ }
247
+ }
248
+
249
+ template <std::size_t... Index>
250
+ static consteval bool has_special_args(std::index_sequence<Index...>) {
251
+ using args = typename resolved_function_traits<Function>::args_tuple;
252
+ return (is_special_argument_v<std::tuple_element_t<Index, args>> || ...);
253
+ }
254
+
255
+ template <std::size_t... Index>
256
+ static consteval bool has_rest_args(std::index_sequence<Index...>) {
257
+ using args = typename resolved_function_traits<Function>::args_tuple;
258
+ return (is_rest_argument_v<std::tuple_element_t<Index, args>> || ...);
259
+ }
260
+
261
+ Function function_;
262
+ argument_parser<typename resolved_function_traits<Function>::args_tuple> parser_;
263
+ };
264
+
265
+ template <typename Result> value dump_result(Result&& result, policy::kind selected) {
266
+ using result_type = Result;
267
+ using bare_type = std::remove_cvref_t<Result>;
268
+ if constexpr (std::is_pointer_v<bare_type> && std::is_class_v<std::remove_pointer_t<bare_type>>) {
269
+ using pointee = std::remove_pointer_t<bare_type>;
270
+ if (result == nullptr) {
271
+ return value{Qnil};
272
+ }
273
+ if (selected == policy::kind::copy) {
274
+ return wrap_copy(*result);
275
+ }
276
+ if (selected == policy::kind::take) {
277
+ return wrap_take(std::unique_ptr<pointee>{result});
278
+ }
279
+ if (selected == policy::kind::shared) {
280
+ throw std::invalid_argument("rbxx: shared policy requires std::shared_ptr<T>");
281
+ }
282
+ return wrap_reference(result);
283
+ } else if constexpr (std::is_lvalue_reference_v<result_type> && std::is_class_v<bare_type>) {
284
+ if (selected == policy::kind::copy) {
285
+ return wrap_copy(result);
286
+ }
287
+ if (selected == policy::kind::take || selected == policy::kind::shared) {
288
+ throw std::invalid_argument("rbxx: take/shared policy cannot be used with a reference");
289
+ }
290
+ return wrap_reference(std::addressof(result));
291
+ } else {
292
+ return to_ruby(std::forward<Result>(result));
293
+ }
294
+ }
295
+
296
+ struct method_key {
297
+ VALUE owner;
298
+ ID method;
299
+
300
+ friend bool operator==(const method_key&, const method_key&) = default;
301
+ };
302
+
303
+ struct method_key_hash {
304
+ [[nodiscard]] std::size_t operator()(const method_key& key) const noexcept {
305
+ const auto owner = static_cast<std::size_t>(key.owner);
306
+ const auto method = static_cast<std::size_t>(key.method);
307
+ return owner ^ (method + 0x9e3779b9U + (owner << 6U) + (owner >> 2U));
308
+ }
309
+ };
310
+
311
+ class method_registry {
312
+ public:
313
+ static method_registry& instance() {
314
+ static auto* registry = new method_registry();
315
+ return *registry;
316
+ }
317
+
318
+ bool add(VALUE owner, ID method, std::unique_ptr<native_function> function) {
319
+ std::lock_guard lock(write_mutex_);
320
+ auto& overloads = functions_[method_key{owner, method}];
321
+ const bool first = overloads.empty();
322
+ overloads.push_back(std::move(function));
323
+ return first;
324
+ }
325
+
326
+ using overloads = std::vector<std::unique_ptr<native_function>>;
327
+
328
+ [[nodiscard]] const overloads* find(VALUE self, ID method) const noexcept {
329
+ if (const auto* direct = find_exact(self, method)) {
330
+ return direct;
331
+ }
332
+
333
+ VALUE klass = CLASS_OF(self);
334
+ while (!NIL_P(klass)) {
335
+ if (const auto* inherited = find_exact(klass, method)) {
336
+ return inherited;
337
+ }
338
+ klass = rb_class_superclass(klass);
339
+ }
340
+ return find_exact(Qnil, method);
341
+ }
342
+
343
+ private:
344
+ [[nodiscard]] const overloads* find_exact(VALUE owner, ID method) const noexcept {
345
+ auto found = functions_.find(method_key{owner, method});
346
+ return found == functions_.end() ? nullptr : std::addressof(found->second);
347
+ }
348
+
349
+ std::mutex write_mutex_;
350
+ std::unordered_map<method_key, overloads, method_key_hash> functions_;
351
+ };
352
+
353
+ inline VALUE function_trampoline(int argc, VALUE* argv, VALUE self) {
354
+ VALUE pending = Qnil;
355
+ VALUE result = Qnil;
356
+ try {
357
+ ID method = rb_frame_this_func();
358
+ const auto* functions = method_registry::instance().find(self, method);
359
+ if (functions == nullptr || functions->empty()) {
360
+ throw std::runtime_error("rbxx: native function registry entry was not found");
361
+ }
362
+ native_function* selected = nullptr;
363
+ int best_score = -1;
364
+ for (const auto& candidate : *functions) {
365
+ int score = candidate->match_score(argc, argv);
366
+ if (score > best_score) {
367
+ best_score = score;
368
+ selected = candidate.get();
369
+ }
370
+ }
371
+ if (selected == nullptr && functions->size() == 1U && functions->front()->accepts_arity(argc)) {
372
+ selected = functions->front().get();
373
+ }
374
+ if (selected == nullptr) {
375
+ std::string message = "rbxx: no matching overload";
376
+ if (functions->size() == 1U) {
377
+ message += "; expected ";
378
+ message += std::to_string(functions->front()->declared_arity());
379
+ message += ", actual ";
380
+ message += std::to_string(argc);
381
+ }
382
+ message += "; candidates:";
383
+ for (const auto& candidate : *functions) {
384
+ message += "\n ";
385
+ message += candidate->signature();
386
+ }
387
+ throw ruby_error(make_exception(rb_eArgError, message.c_str()));
388
+ }
389
+ result = selected->invoke(argc, argv, value{self}).raw();
390
+ } catch (...) {
391
+ pending = translate_current_exception();
392
+ }
393
+ if (!NIL_P(pending)) {
394
+ rb_exc_raise(pending);
395
+ }
396
+ return result;
397
+ }
398
+
399
+ template <typename Function>
400
+ void register_function(VALUE owner, const char* name, Function&& function,
401
+ std::vector<argument_spec> specs = {}, bool global = false) {
402
+ using stored_function = std::decay_t<Function>;
403
+ if constexpr (!function_signature<stored_function>) {
404
+ static_assert(
405
+ function_signature<stored_function>,
406
+ "rbxx: callable signature cannot be determined; use a non-generic lambda, function "
407
+ "pointer, or std::function");
408
+ } else if constexpr (!function_arguments_convertible<stored_function>()) {
409
+ static_assert(
410
+ function_arguments_convertible<stored_function>(),
411
+ "rbxx: function argument type has no type_caster; bind the type with def_class<T>() "
412
+ "or specialize rbxx::type_caster<T>");
413
+ } else if constexpr (!function_return_convertible<stored_function>()) {
414
+ static_assert(
415
+ function_return_convertible<stored_function>(),
416
+ "rbxx: function return type has no type_caster; bind the type with def_class<T>() "
417
+ "or specialize rbxx::type_caster<T>");
418
+ } else {
419
+ ID method = protect(rb_intern, name);
420
+ const bool first =
421
+ method_registry::instance().add(global ? Qnil : owner, method,
422
+ std::make_unique<native_function_impl<stored_function>>(
423
+ std::forward<Function>(function), std::move(specs)));
424
+ if (first) {
425
+ protect([owner, name, global] {
426
+ if (global) {
427
+ rb_define_global_function(name, function_trampoline, -1);
428
+ } else {
429
+ rb_define_module_function(owner, name, function_trampoline, -1);
430
+ }
431
+ });
432
+ }
433
+ }
434
+ }
435
+
436
+ template <typename Function>
437
+ void register_static_function(VALUE owner, const char* name, Function&& function,
438
+ std::vector<argument_spec> specs = {}) {
439
+ using stored_function = std::decay_t<Function>;
440
+ if constexpr (!function_signature<stored_function>) {
441
+ static_assert(
442
+ function_signature<stored_function>,
443
+ "rbxx: callable signature cannot be determined; use a non-generic lambda, function "
444
+ "pointer, or std::function");
445
+ } else if constexpr (!function_arguments_convertible<stored_function>()) {
446
+ static_assert(function_arguments_convertible<stored_function>(),
447
+ "rbxx: function argument type has no type_caster");
448
+ } else if constexpr (!function_return_convertible<stored_function>()) {
449
+ static_assert(function_return_convertible<stored_function>(),
450
+ "rbxx: function return type has no type_caster");
451
+ } else {
452
+ ID method = protect(rb_intern, name);
453
+ const bool first =
454
+ method_registry::instance().add(owner, method,
455
+ std::make_unique<native_function_impl<stored_function>>(
456
+ std::forward<Function>(function), std::move(specs)));
457
+ if (first) {
458
+ protect([owner, name] { rb_define_singleton_method(owner, name, function_trampoline, -1); });
459
+ }
460
+ }
461
+ }
462
+
463
+ } // namespace rbxx::detail
@@ -0,0 +1,69 @@
1
+ #pragma once
2
+
3
+ #include <rbxx/function.hpp>
4
+ #include <rbxx/operators.hpp>
5
+
6
+ #include <source_location>
7
+ #include <utility>
8
+
9
+ namespace rbxx {
10
+
11
+ template <typename T> class class_;
12
+
13
+ /// @brief A non-owning DSL handle for a Ruby Module.
14
+ /// @code rbxx::define_module("Demo").def("answer", [] { return 42; }); @endcode
15
+ class module {
16
+ public:
17
+ /// @brief Wraps an existing Ruby module VALUE.
18
+ explicit module(value wrapped) noexcept : wrapped_(wrapped) {}
19
+
20
+ /// @brief Returns the wrapped Ruby module.
21
+ [[nodiscard]] value get() const noexcept { return wrapped_; }
22
+
23
+ /// @brief Defines a Ruby module function backed by a C++ callable.
24
+ /// @code mod.def("sum", [](int a, int b) { return a + b; }); @endcode
25
+ template <typename Function, typename... Specs>
26
+ module& def(const char* name, Function&& function, Specs&&... specs) {
27
+ using traits = detail::resolved_function_traits<std::decay_t<Function>>;
28
+ if constexpr (detail::function_signature<std::decay_t<Function>>) {
29
+ static_assert(sizeof...(Specs) == 0U ||
30
+ sizeof...(Specs) ==
31
+ detail::normal_argument_count<typename traits::args_tuple>(),
32
+ "rbxx: argument annotation count must match callable parameters");
33
+ }
34
+ detail::register_function(wrapped_.raw(), name, std::forward<Function>(function),
35
+ detail::make_argument_specs(std::forward<Specs>(specs)...));
36
+ return *this;
37
+ }
38
+
39
+ template <typename Function, typename... Specs>
40
+ module& def(op::name operation, Function&& function, Specs&&... specs) {
41
+ return def(operation.ruby_name, std::forward<Function>(function),
42
+ std::forward<Specs>(specs)...);
43
+ }
44
+
45
+ /// @brief Defines a Ruby class backed by CRuby TypedData.
46
+ /// @code auto counter = mod.def_class<Counter>("Counter"); @endcode
47
+ template <typename T, typename Base = void>
48
+ class_<T> def_class(const char* name,
49
+ std::source_location location = std::source_location::current());
50
+
51
+ private:
52
+ value wrapped_;
53
+ };
54
+
55
+ /// @brief Defines or reopens a top-level Ruby module.
56
+ /// @code rbxx::module demo = rbxx::define_module("Demo"); @endcode
57
+ [[nodiscard]] inline module define_module(const char* name) {
58
+ return module{value{protect(rb_define_module, name)}};
59
+ }
60
+
61
+ /// @brief Defines a private Kernel method callable as a Ruby global function.
62
+ /// @code rbxx::define_global_function("native_sum", [](int a, int b) { return a + b; }); @endcode
63
+ template <typename Function, typename... Specs>
64
+ void define_global_function(const char* name, Function&& function, Specs&&... specs) {
65
+ detail::register_function(Qnil, name, std::forward<Function>(function),
66
+ detail::make_argument_specs(std::forward<Specs>(specs)...), true);
67
+ }
68
+
69
+ } // namespace rbxx
@@ -0,0 +1,146 @@
1
+ #pragma once
2
+
3
+ #include <rbxx/function.hpp>
4
+
5
+ #include <exception>
6
+ #include <functional>
7
+ #include <optional>
8
+ #include <stdexcept>
9
+ #include <tuple>
10
+ #include <type_traits>
11
+ #include <utility>
12
+
13
+ namespace rbxx {
14
+ namespace detail {
15
+
16
+ struct default_unblock_function {};
17
+
18
+ template <typename T> struct is_std_function : std::false_type {};
19
+ template <typename Return, typename... Args>
20
+ struct is_std_function<std::function<Return(Args...)>> : std::true_type {};
21
+
22
+ template <typename T>
23
+ inline constexpr bool gvl_independent_v = !std::is_same_v<std::remove_cvref_t<T>, value> &&
24
+ !std::is_same_v<std::remove_cvref_t<T>, object> &&
25
+ !std::is_same_v<std::remove_cvref_t<T>, block> &&
26
+ !std::is_same_v<std::remove_cvref_t<T>, optional_block> &&
27
+ !std::is_same_v<std::remove_cvref_t<T>, args> &&
28
+ !is_std_function<std::remove_cvref_t<T>>::value;
29
+
30
+ template <typename Function, typename Unblock, typename Return, typename Tuple>
31
+ class nogvl_adapter_impl;
32
+
33
+ template <typename Function, typename Unblock, typename Return, typename... Args>
34
+ class nogvl_adapter_impl<Function, Unblock, Return, std::tuple<Args...>> {
35
+ public:
36
+ explicit nogvl_adapter_impl(Function function) : function_(std::move(function)) {
37
+ validate_signature();
38
+ }
39
+
40
+ nogvl_adapter_impl(Function function, Unblock unblock)
41
+ : function_(std::move(function)), unblock_(std::move(unblock)) {
42
+ validate_signature();
43
+ static_assert(std::is_nothrow_invocable_v<Unblock&>,
44
+ "rbxx: nogvl interrupt function must be noexcept");
45
+ }
46
+
47
+ Return operator()(Args... args) {
48
+ payload work{this, std::tuple<Args...>{std::forward<Args>(args)...}, std::nullopt, nullptr};
49
+ if constexpr (std::is_same_v<Unblock, default_unblock_function>) {
50
+ protect([&work] { rb_thread_call_without_gvl(run, &work, RUBY_UBF_IO, nullptr); });
51
+ } else {
52
+ protect([&work, this] {
53
+ rb_thread_call_without_gvl(run, &work, interrupt, std::addressof(unblock_));
54
+ });
55
+ }
56
+ if (work.exception) {
57
+ std::rethrow_exception(work.exception);
58
+ }
59
+ if constexpr (!std::is_void_v<Return>) {
60
+ if (!work.result) {
61
+ throw std::runtime_error("rbxx: nogvl function was interrupted before producing a result");
62
+ }
63
+ return std::move(*work.result);
64
+ }
65
+ }
66
+
67
+ private:
68
+ using stored_result = std::conditional_t<std::is_void_v<Return>, bool, Return>;
69
+
70
+ struct payload {
71
+ nogvl_adapter_impl* adapter;
72
+ std::tuple<Args...> arguments;
73
+ std::optional<stored_result> result;
74
+ std::exception_ptr exception;
75
+ };
76
+
77
+ static consteval void validate_signature() {
78
+ static_assert(!std::is_reference_v<Return>,
79
+ "rbxx: nogvl return values must be owned C++ values");
80
+ static_assert(gvl_independent_v<Return>,
81
+ "rbxx: nogvl return type must not access Ruby without the GVL");
82
+ static_assert((gvl_independent_v<Args> && ...),
83
+ "rbxx: nogvl arguments must not access Ruby without the GVL");
84
+ }
85
+
86
+ static void* run(void* opaque) noexcept {
87
+ auto* work = static_cast<payload*>(opaque);
88
+ try {
89
+ if constexpr (std::is_void_v<Return>) {
90
+ std::apply(
91
+ [&](auto&&... values) {
92
+ std::invoke(work->adapter->function_, std::forward<Args>(values)...);
93
+ },
94
+ work->arguments);
95
+ work->result.emplace(true);
96
+ } else {
97
+ work->result.emplace(std::apply(
98
+ [&](auto&&... values) -> Return {
99
+ return std::invoke(work->adapter->function_, std::forward<Args>(values)...);
100
+ },
101
+ work->arguments));
102
+ }
103
+ } catch (...) {
104
+ work->exception = std::current_exception();
105
+ }
106
+ return nullptr;
107
+ }
108
+
109
+ static void interrupt(void* opaque) noexcept { std::invoke(*static_cast<Unblock*>(opaque)); }
110
+
111
+ Function function_;
112
+ [[no_unique_address]] Unblock unblock_{};
113
+ };
114
+
115
+ template <typename Function, typename Unblock = default_unblock_function>
116
+ using nogvl_adapter =
117
+ nogvl_adapter_impl<std::decay_t<Function>, std::decay_t<Unblock>,
118
+ typename resolved_function_traits<std::decay_t<Function>>::return_type,
119
+ typename resolved_function_traits<std::decay_t<Function>>::args_tuple>;
120
+
121
+ } // namespace detail
122
+
123
+ /// @brief Wraps a pure C++ callable so its execution occurs without the Ruby GVL.
124
+ template <typename Function> [[nodiscard]] auto nogvl(Function&& function) {
125
+ using stored_function = std::decay_t<Function>;
126
+ static_assert(detail::function_signature<stored_function>,
127
+ "rbxx: nogvl requires a callable with a concrete signature");
128
+ static_assert(!std::is_member_function_pointer_v<stored_function>,
129
+ "rbxx: nogvl expects a free function or callable object");
130
+ return detail::nogvl_adapter<stored_function>{std::forward<Function>(function)};
131
+ }
132
+
133
+ /// @brief Wraps a pure C++ callable with a noexcept interruption hook.
134
+ template <typename Function, typename Unblock>
135
+ [[nodiscard]] auto nogvl_interruptible(Function&& function, Unblock&& unblock) {
136
+ using stored_function = std::decay_t<Function>;
137
+ using stored_unblock = std::decay_t<Unblock>;
138
+ static_assert(detail::function_signature<stored_function>,
139
+ "rbxx: nogvl_interruptible requires a callable with a concrete signature");
140
+ static_assert(!std::is_member_function_pointer_v<stored_function>,
141
+ "rbxx: nogvl_interruptible expects a free function or callable object");
142
+ return detail::nogvl_adapter<stored_function, stored_unblock>{std::forward<Function>(function),
143
+ std::forward<Unblock>(unblock)};
144
+ }
145
+
146
+ } // namespace rbxx