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.
- checksums.yaml +7 -0
- data/README.md +72 -0
- data/exe/rbxx +6 -0
- data/ext-cmake/FindRuby.cmake +42 -0
- data/ext-cmake/rbxx-config.cmake +25 -0
- data/include/rbxx/arg.hpp +347 -0
- data/include/rbxx/callback.hpp +54 -0
- data/include/rbxx/class.hpp +630 -0
- data/include/rbxx/data_object.hpp +221 -0
- data/include/rbxx/detail/ruby_include.hpp +36 -0
- data/include/rbxx/exception.hpp +116 -0
- data/include/rbxx/extension.hpp +23 -0
- data/include/rbxx/function.hpp +463 -0
- data/include/rbxx/module.hpp +69 -0
- data/include/rbxx/nogvl.hpp +146 -0
- data/include/rbxx/object.hpp +68 -0
- data/include/rbxx/operators.hpp +31 -0
- data/include/rbxx/policies.hpp +36 -0
- data/include/rbxx/protect.hpp +72 -0
- data/include/rbxx/rbxx.hpp +20 -0
- data/include/rbxx/registry.hpp +145 -0
- data/include/rbxx/stl/array.hpp +40 -0
- data/include/rbxx/stl/chrono.hpp +26 -0
- data/include/rbxx/stl/detail.hpp +60 -0
- data/include/rbxx/stl/filesystem.hpp +21 -0
- data/include/rbxx/stl/map.hpp +49 -0
- data/include/rbxx/stl/optional.hpp +23 -0
- data/include/rbxx/stl/set.hpp +30 -0
- data/include/rbxx/stl/tuple.hpp +74 -0
- data/include/rbxx/stl/variant.hpp +35 -0
- data/include/rbxx/stl/vector.hpp +34 -0
- data/include/rbxx/stl.hpp +11 -0
- data/include/rbxx/type_caster.hpp +247 -0
- data/include/rbxx/value.hpp +63 -0
- data/include/rbxx/version.hpp +14 -0
- data/lib/rbxx/cli.rb +127 -0
- data/lib/rbxx/mkmf.rb +144 -0
- data/lib/rbxx/rake_tasks.rb +44 -0
- data/lib/rbxx/version.rb +5 -0
- data/lib/rbxx.rb +8 -0
- data/single_include/rbxx/rbxx.hpp +3211 -0
- data/templates/CMakeLists.txt.erb +14 -0
- data/templates/Gemfile.erb +10 -0
- data/templates/README.md.erb +11 -0
- data/templates/Rakefile.erb +23 -0
- data/templates/ci.yml.erb +21 -0
- data/templates/extconf.rb.erb +5 -0
- data/templates/extconf_cmake.rb.erb +5 -0
- data/templates/extension.cpp.erb +23 -0
- data/templates/gemspec.erb +16 -0
- data/templates/lib.rb.erb +4 -0
- data/templates/release.yml.erb +21 -0
- data/templates/test.rb.erb +12 -0
- data/templates/version.rb.erb +5 -0
- metadata +99 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6f2741fa6b9570fd7415917ba76ee1f64d58797290e03e311d001c72d5627978
|
|
4
|
+
data.tar.gz: 1c7d7492ca0eea06e60c8a40a502942c77ce9440fcd326b2199f210b860e69de
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6250c53865269739bab2f9f0813625a2e33c4f12eb60cb9562aaca3e4431d3a8db3f7e8e226c03ff044d549159de93af53857a0d3d6392e003cce65038819393
|
|
7
|
+
data.tar.gz: 51cf36c2fdbe73ae386528cb59b46ad13c978734cdea13715c18768adb7d9b26c8fe0156b9897bc5d4c5eaabeeb2e871673a012f2d523b8b5877c595dbff83c3
|
data/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# rbxx
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ydah/rbxx/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
rbxx is a header-only C++20 binding library for CRuby native extensions. It combines explicit
|
|
6
|
+
Ruby/C++ exception boundaries, TypedData ownership, STL conversion, keyword arguments, callbacks,
|
|
7
|
+
GVL release, and source/precompiled gem tooling.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
Install rbxx, then generate a native gem:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
gem install rbxx
|
|
15
|
+
rbxx new counter_ext
|
|
16
|
+
cd counter_ext
|
|
17
|
+
bundle exec rake test
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A complete class binding stays small:
|
|
21
|
+
|
|
22
|
+
```cpp
|
|
23
|
+
#include <rbxx/rbxx.hpp>
|
|
24
|
+
class Counter {
|
|
25
|
+
public:
|
|
26
|
+
explicit Counter(int n) : n_(n) {}
|
|
27
|
+
void add(int d) { n_ += d; }
|
|
28
|
+
int value() const { return n_; }
|
|
29
|
+
private:
|
|
30
|
+
int n_;
|
|
31
|
+
};
|
|
32
|
+
RBXX_EXTENSION(counter_ext) {
|
|
33
|
+
rbxx::define_module("Demo").def_class<Counter>("Counter")
|
|
34
|
+
.def(rbxx::init<int>(), rbxx::kwarg("start") = 0)
|
|
35
|
+
.def("add", &Counter::add, rbxx::arg("delta"))
|
|
36
|
+
.def<&Counter::value>("value");
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use `def<&T::method>` for the fixed-arity zero-argument fast path. Ordinary `.def(...)` supports
|
|
41
|
+
arguments, defaults, keywords, blocks, overloads, policies, and lifetime rules.
|
|
42
|
+
|
|
43
|
+
## Build integrations
|
|
44
|
+
|
|
45
|
+
- `require "rbxx/mkmf"` and `create_rbxx_makefile("gem/extension")`
|
|
46
|
+
- `find_package(rbxx CONFIG REQUIRED)` and the `rbxx::rbxx` CMake target
|
|
47
|
+
- `rbxx new NAME [--cmake]` for tested gem scaffolding
|
|
48
|
+
- `Rbxx::RakeTasks` for five rake-compiler-dock targets
|
|
49
|
+
- `single_include/rbxx/rbxx.hpp` when a generated single header is preferable
|
|
50
|
+
|
|
51
|
+
See [the tutorial](docs/tutorial.md), [API overview](docs/api.md), [examples](examples), and
|
|
52
|
+
[benchmark results](docs/benchmarks.md).
|
|
53
|
+
|
|
54
|
+
## Supported scope and limitations
|
|
55
|
+
|
|
56
|
+
- CRuby 3.1 and newer; C++20; GCC 12+, Clang 15+, AppleClang 15+, and MSVC 2022.
|
|
57
|
+
- Version 0.1 is main-Ractor only and does not call `rb_ext_ractor_safe`.
|
|
58
|
+
- Embedding Ruby in a C++ executable is not a supported v0.1 workflow.
|
|
59
|
+
- mruby, TruffleRuby, JRuby, C++ modules, and binding auto-generation are out of scope.
|
|
60
|
+
- Iterating a container while mutating it from Ruby has undefined behavior.
|
|
61
|
+
- A Ruby callback may only run on a Ruby thread holding the GVL.
|
|
62
|
+
|
|
63
|
+
## Development
|
|
64
|
+
|
|
65
|
+
Run `bundle exec rake compile:test test test:compile_fail`, `bundle exec rake test:gc_stress`,
|
|
66
|
+
`bundle exec rake lint`, and `bundle exec rake amalgamate:check amalgamate:smoke` before release.
|
|
67
|
+
Sanitizer CI uses `RBXX_SANITIZE=1`; intentional UAF reproduction is gated by
|
|
68
|
+
`RBXX_DANGER_TESTS=1` and is never part of the normal suite.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
rbxx is available under the MIT License.
|
data/exe/rbxx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Find CRuby through the selected interpreter's RbConfig.
|
|
2
|
+
|
|
3
|
+
if(NOT Ruby_EXECUTABLE)
|
|
4
|
+
find_program(Ruby_EXECUTABLE NAMES ruby REQUIRED)
|
|
5
|
+
endif()
|
|
6
|
+
|
|
7
|
+
execute_process(
|
|
8
|
+
COMMAND "${Ruby_EXECUTABLE}" -rrbconfig -e
|
|
9
|
+
"puts %w[rubyhdrdir rubyarchhdrdir libdir LIBRUBY_SO DLEXT].map { |key| RbConfig::CONFIG[key] }"
|
|
10
|
+
RESULT_VARIABLE _ruby_config_status
|
|
11
|
+
OUTPUT_VARIABLE _ruby_config
|
|
12
|
+
OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if(NOT _ruby_config_status EQUAL 0)
|
|
16
|
+
message(FATAL_ERROR "RbConfig failed for ${Ruby_EXECUTABLE}")
|
|
17
|
+
endif()
|
|
18
|
+
|
|
19
|
+
string(REPLACE "\n" ";" _ruby_values "${_ruby_config}")
|
|
20
|
+
list(LENGTH _ruby_values _ruby_value_count)
|
|
21
|
+
if(_ruby_value_count LESS 5)
|
|
22
|
+
message(FATAL_ERROR "RbConfig returned incomplete extension build settings")
|
|
23
|
+
endif()
|
|
24
|
+
|
|
25
|
+
list(GET _ruby_values 0 Ruby_INCLUDE_DIR)
|
|
26
|
+
list(GET _ruby_values 1 Ruby_ARCH_INCLUDE_DIR)
|
|
27
|
+
list(GET _ruby_values 2 Ruby_LIBRARY_DIR)
|
|
28
|
+
list(GET _ruby_values 3 Ruby_LIBRARY_NAME)
|
|
29
|
+
list(GET _ruby_values 4 Ruby_DLEXT)
|
|
30
|
+
set(Ruby_INCLUDE_DIRS "${Ruby_INCLUDE_DIR};${Ruby_ARCH_INCLUDE_DIR}")
|
|
31
|
+
|
|
32
|
+
if(WIN32)
|
|
33
|
+
find_library(Ruby_LIBRARY NAMES "${Ruby_LIBRARY_NAME}" PATHS "${Ruby_LIBRARY_DIR}" REQUIRED)
|
|
34
|
+
endif()
|
|
35
|
+
|
|
36
|
+
include(FindPackageHandleStandardArgs)
|
|
37
|
+
find_package_handle_standard_args(
|
|
38
|
+
Ruby
|
|
39
|
+
REQUIRED_VARS Ruby_EXECUTABLE Ruby_INCLUDE_DIR Ruby_ARCH_INCLUDE_DIR Ruby_DLEXT
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
mark_as_advanced(Ruby_INCLUDE_DIR Ruby_ARCH_INCLUDE_DIR Ruby_LIBRARY_DIR Ruby_LIBRARY_NAME)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}")
|
|
2
|
+
find_package(Ruby REQUIRED MODULE)
|
|
3
|
+
|
|
4
|
+
if(NOT TARGET rbxx::rbxx)
|
|
5
|
+
add_library(rbxx::rbxx INTERFACE IMPORTED)
|
|
6
|
+
get_filename_component(_rbxx_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
|
7
|
+
set_target_properties(rbxx::rbxx PROPERTIES
|
|
8
|
+
INTERFACE_COMPILE_FEATURES cxx_std_20
|
|
9
|
+
INTERFACE_INCLUDE_DIRECTORIES "${_rbxx_root}/include;${Ruby_INCLUDE_DIRS}"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
if(MSVC)
|
|
13
|
+
target_compile_options(rbxx::rbxx INTERFACE /EHsc /utf-8 /W4)
|
|
14
|
+
else()
|
|
15
|
+
target_compile_options(rbxx::rbxx INTERFACE -Wall -Wextra -fvisibility=hidden)
|
|
16
|
+
if(APPLE)
|
|
17
|
+
target_link_options(rbxx::rbxx INTERFACE -undefined dynamic_lookup)
|
|
18
|
+
endif()
|
|
19
|
+
endif()
|
|
20
|
+
if(WIN32)
|
|
21
|
+
target_link_libraries(rbxx::rbxx INTERFACE "${Ruby_LIBRARY}")
|
|
22
|
+
endif()
|
|
23
|
+
endif()
|
|
24
|
+
|
|
25
|
+
set(rbxx_FOUND TRUE)
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <rbxx/type_caster.hpp>
|
|
4
|
+
|
|
5
|
+
#include <algorithm>
|
|
6
|
+
#include <array>
|
|
7
|
+
#include <cstddef>
|
|
8
|
+
#include <string>
|
|
9
|
+
#include <tuple>
|
|
10
|
+
#include <type_traits>
|
|
11
|
+
#include <utility>
|
|
12
|
+
#include <vector>
|
|
13
|
+
|
|
14
|
+
namespace rbxx {
|
|
15
|
+
|
|
16
|
+
struct argument_spec {
|
|
17
|
+
std::string name;
|
|
18
|
+
bool keyword = false;
|
|
19
|
+
bool has_default = false;
|
|
20
|
+
object default_value;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/// @brief Declares a named positional argument and an optional default.
|
|
24
|
+
/// @code function.def("add", fn, rbxx::arg("delta") = 1); @endcode
|
|
25
|
+
class arg {
|
|
26
|
+
public:
|
|
27
|
+
explicit arg(const char* name) : name_(name) {}
|
|
28
|
+
operator argument_spec() const { return argument_spec{name_, false, false, {}}; }
|
|
29
|
+
|
|
30
|
+
template <typename T> argument_spec operator=(T&& default_value) const {
|
|
31
|
+
return argument_spec{name_, false, true, object{to_ruby(std::forward<T>(default_value))}};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private:
|
|
35
|
+
std::string name_;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/// @brief Declares a keyword argument and an optional default.
|
|
39
|
+
/// @code function.def("open", fn, rbxx::kwarg("mode") = "fast"); @endcode
|
|
40
|
+
class kwarg {
|
|
41
|
+
public:
|
|
42
|
+
explicit kwarg(const char* name) : name_(name) {}
|
|
43
|
+
operator argument_spec() const { return argument_spec{name_, true, false, {}}; }
|
|
44
|
+
|
|
45
|
+
template <typename T> argument_spec operator=(T&& default_value) const {
|
|
46
|
+
return argument_spec{name_, true, true, object{to_ruby(std::forward<T>(default_value))}};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private:
|
|
50
|
+
std::string name_;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/// @brief Required Ruby block passed to a bound C++ callable.
|
|
54
|
+
class block {
|
|
55
|
+
public:
|
|
56
|
+
explicit block(value callable) : callable_(callable) {}
|
|
57
|
+
|
|
58
|
+
[[nodiscard]] value get() const noexcept { return callable_.get(); }
|
|
59
|
+
|
|
60
|
+
template <typename Return = value, typename... Args> Return call(Args&&... args) const {
|
|
61
|
+
std::array<VALUE, sizeof...(Args)> arguments{to_ruby(std::forward<Args>(args)).raw()...};
|
|
62
|
+
VALUE result = protect(rb_funcallv, callable_.raw(), rb_intern("call"),
|
|
63
|
+
static_cast<int>(sizeof...(Args)), arguments.data());
|
|
64
|
+
if constexpr (std::is_same_v<Return, value>) {
|
|
65
|
+
return value{result};
|
|
66
|
+
} else {
|
|
67
|
+
return from_ruby<Return>(value{result});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private:
|
|
72
|
+
object callable_;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/// @brief Optional Ruby block passed to a bound C++ callable.
|
|
76
|
+
class optional_block {
|
|
77
|
+
public:
|
|
78
|
+
optional_block() = default;
|
|
79
|
+
explicit optional_block(value callable) : callable_(callable) {}
|
|
80
|
+
|
|
81
|
+
[[nodiscard]] explicit operator bool() const noexcept { return !callable_.is_nil(); }
|
|
82
|
+
[[nodiscard]] value get() const noexcept { return callable_.get(); }
|
|
83
|
+
|
|
84
|
+
template <typename Return = value, typename... Args> Return call(Args&&... args) const {
|
|
85
|
+
if (!*this) {
|
|
86
|
+
throw std::invalid_argument("rbxx: optional block is not present");
|
|
87
|
+
}
|
|
88
|
+
return block{callable_.get()}.template call<Return>(std::forward<Args>(args)...);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private:
|
|
92
|
+
object callable_;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/// @brief Remaining positional Ruby arguments.
|
|
96
|
+
class args {
|
|
97
|
+
public:
|
|
98
|
+
explicit args(std::vector<value> values) : values_(std::move(values)) {}
|
|
99
|
+
[[nodiscard]] const std::vector<value>& values() const noexcept { return values_; }
|
|
100
|
+
[[nodiscard]] std::size_t size() const noexcept { return values_.size(); }
|
|
101
|
+
[[nodiscard]] value operator[](std::size_t index) const { return values_.at(index); }
|
|
102
|
+
|
|
103
|
+
private:
|
|
104
|
+
std::vector<value> values_;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
template <> struct type_caster<block> {
|
|
108
|
+
static block load(value input) { return block{input}; }
|
|
109
|
+
static value dump(const block& input) noexcept { return input.get(); }
|
|
110
|
+
static bool matches(value input) noexcept { return RTEST(rb_obj_is_proc(input.raw())); }
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
template <> struct type_caster<optional_block> {
|
|
114
|
+
static optional_block load(value input) {
|
|
115
|
+
return input.is_nil() ? optional_block{} : optional_block{input};
|
|
116
|
+
}
|
|
117
|
+
static value dump(const optional_block& input) noexcept {
|
|
118
|
+
return input ? input.get() : value{Qnil};
|
|
119
|
+
}
|
|
120
|
+
static bool matches(value input) noexcept {
|
|
121
|
+
return input.is_nil() || RTEST(rb_obj_is_proc(input.raw()));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
template <> struct type_caster<args> {
|
|
126
|
+
static args load(value input) { return args{{input}}; }
|
|
127
|
+
static value dump(const args& input) {
|
|
128
|
+
return value{protect([&input] {
|
|
129
|
+
VALUE result = rb_ary_new_capa(static_cast<long>(input.size()));
|
|
130
|
+
for (value element : input.values()) {
|
|
131
|
+
rb_ary_push(result, element.raw());
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
})};
|
|
135
|
+
}
|
|
136
|
+
static bool matches(value) noexcept { return true; }
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
namespace detail {
|
|
140
|
+
|
|
141
|
+
template <typename Argument> auto load_argument(value input) {
|
|
142
|
+
using loaded_type = decltype(from_ruby<Argument>(input));
|
|
143
|
+
if constexpr (std::is_lvalue_reference_v<loaded_type>) {
|
|
144
|
+
return std::ref(from_ruby<Argument>(input));
|
|
145
|
+
} else {
|
|
146
|
+
return from_ruby<Argument>(input);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
template <typename T>
|
|
151
|
+
inline constexpr bool is_block_argument_v = std::is_same_v<std::remove_cvref_t<T>, block>;
|
|
152
|
+
template <typename T>
|
|
153
|
+
inline constexpr bool is_optional_block_argument_v =
|
|
154
|
+
std::is_same_v<std::remove_cvref_t<T>, optional_block>;
|
|
155
|
+
template <typename T>
|
|
156
|
+
inline constexpr bool is_rest_argument_v = std::is_same_v<std::remove_cvref_t<T>, args>;
|
|
157
|
+
template <typename T>
|
|
158
|
+
inline constexpr bool is_special_argument_v =
|
|
159
|
+
is_block_argument_v<T> || is_optional_block_argument_v<T> || is_rest_argument_v<T>;
|
|
160
|
+
|
|
161
|
+
template <typename Tuple, std::size_t... Index>
|
|
162
|
+
consteval std::size_t normal_argument_count(std::index_sequence<Index...>) {
|
|
163
|
+
return (std::size_t{0} + ... +
|
|
164
|
+
(is_special_argument_v<std::tuple_element_t<Index, Tuple>> ? 0U : 1U));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
template <typename Tuple> consteval std::size_t normal_argument_count() {
|
|
168
|
+
return normal_argument_count<Tuple>(std::make_index_sequence<std::tuple_size_v<Tuple>>{});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
inline std::vector<argument_spec> make_argument_specs() { return {}; }
|
|
172
|
+
|
|
173
|
+
template <typename... Specs> std::vector<argument_spec> make_argument_specs(Specs&&... specs) {
|
|
174
|
+
static_assert((std::is_convertible_v<Specs, argument_spec> && ...),
|
|
175
|
+
"rbxx: binding options must be arg(...) or kwarg(...)");
|
|
176
|
+
std::vector<argument_spec> result;
|
|
177
|
+
result.reserve(sizeof...(Specs));
|
|
178
|
+
(result.emplace_back(static_cast<argument_spec>(std::forward<Specs>(specs))), ...);
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
struct parsed_arguments {
|
|
183
|
+
std::vector<value> slots;
|
|
184
|
+
std::vector<value> rest;
|
|
185
|
+
value block_value{Qnil};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
template <typename Tuple> class argument_parser {
|
|
189
|
+
public:
|
|
190
|
+
static constexpr std::size_t arity = std::tuple_size_v<Tuple>;
|
|
191
|
+
|
|
192
|
+
explicit argument_parser(std::vector<argument_spec> specs) : specs_(std::move(specs)) {
|
|
193
|
+
constexpr std::size_t normal = normal_argument_count<Tuple>();
|
|
194
|
+
if (specs_.empty()) {
|
|
195
|
+
specs_.reserve(normal);
|
|
196
|
+
for (std::size_t index = 0; index < normal; ++index) {
|
|
197
|
+
specs_.push_back(argument_spec{"arg" + std::to_string(index + 1U), false, false, {}});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (specs_.size() != normal) {
|
|
201
|
+
throw std::invalid_argument("rbxx: argument annotations must match non-block parameters");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
[[nodiscard]] parsed_arguments parse(int argc, const VALUE* argv) const {
|
|
206
|
+
parsed_arguments parsed;
|
|
207
|
+
parsed.slots.resize(arity, value{Qundef});
|
|
208
|
+
|
|
209
|
+
int positional_count = argc;
|
|
210
|
+
VALUE keywords = Qnil;
|
|
211
|
+
if (argc > 0 && rb_keyword_given_p()) {
|
|
212
|
+
keywords = argv[argc - 1];
|
|
213
|
+
--positional_count;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
std::vector<VALUE> keyword_values(specs_.size(), Qundef);
|
|
217
|
+
read_keywords(keywords, keyword_values);
|
|
218
|
+
fill_slots(parsed, positional_count, argv, keyword_values, std::make_index_sequence<arity>{});
|
|
219
|
+
read_block(parsed);
|
|
220
|
+
return parsed;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
[[nodiscard]] bool configured() const noexcept {
|
|
224
|
+
return std::any_of(specs_.begin(), specs_.end(),
|
|
225
|
+
[](const argument_spec& spec) { return spec.keyword || spec.has_default; });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private:
|
|
229
|
+
void read_keywords(VALUE keywords, std::vector<VALUE>& values) const {
|
|
230
|
+
std::vector<ID> required;
|
|
231
|
+
std::vector<ID> optional;
|
|
232
|
+
std::vector<std::size_t> required_order;
|
|
233
|
+
std::vector<std::size_t> optional_order;
|
|
234
|
+
for (std::size_t index = 0; index < specs_.size(); ++index) {
|
|
235
|
+
const auto& spec = specs_[index];
|
|
236
|
+
if (!spec.keyword) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
ID id = protect(rb_intern, spec.name.c_str());
|
|
240
|
+
(spec.has_default ? optional : required).push_back(id);
|
|
241
|
+
(spec.has_default ? optional_order : required_order).push_back(index);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (required.empty() && optional.empty()) {
|
|
245
|
+
if (!NIL_P(keywords) && RHASH_SIZE(keywords) != 0) {
|
|
246
|
+
throw ruby_error(make_exception(rb_eArgError, "rbxx: unknown keyword arguments"));
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (NIL_P(keywords)) {
|
|
252
|
+
keywords = protect(rb_hash_new);
|
|
253
|
+
}
|
|
254
|
+
std::vector<ID> table = required;
|
|
255
|
+
table.insert(table.end(), optional.begin(), optional.end());
|
|
256
|
+
std::vector<std::size_t> order = required_order;
|
|
257
|
+
order.insert(order.end(), optional_order.begin(), optional_order.end());
|
|
258
|
+
std::vector<VALUE> extracted(table.size(), Qundef);
|
|
259
|
+
protect([&] {
|
|
260
|
+
return rb_get_kwargs(keywords, table.data(), static_cast<int>(required.size()),
|
|
261
|
+
static_cast<int>(optional.size()), extracted.data());
|
|
262
|
+
});
|
|
263
|
+
for (std::size_t index = 0; index < order.size(); ++index) {
|
|
264
|
+
values[order[index]] = extracted[index];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
template <std::size_t... Index>
|
|
269
|
+
void fill_slots(parsed_arguments& parsed, int positional_count, const VALUE* argv,
|
|
270
|
+
const std::vector<VALUE>& keyword_values, std::index_sequence<Index...>) const {
|
|
271
|
+
std::size_t spec_index = 0;
|
|
272
|
+
int positional_index = 0;
|
|
273
|
+
(fill_slot<Index>(parsed, positional_count, positional_index, argv, spec_index, keyword_values),
|
|
274
|
+
...);
|
|
275
|
+
if (positional_index < positional_count && !has_rest(std::make_index_sequence<arity>{})) {
|
|
276
|
+
throw ruby_error(make_exception(rb_eArgError, "rbxx: too many positional arguments"));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
template <std::size_t Index>
|
|
281
|
+
void fill_slot(parsed_arguments& parsed, int positional_count, int& positional_index,
|
|
282
|
+
const VALUE* argv, std::size_t& spec_index,
|
|
283
|
+
const std::vector<VALUE>& keyword_values) const {
|
|
284
|
+
using argument = std::tuple_element_t<Index, Tuple>;
|
|
285
|
+
if constexpr (is_rest_argument_v<argument>) {
|
|
286
|
+
while (positional_index < positional_count) {
|
|
287
|
+
parsed.rest.emplace_back(argv[positional_index++]);
|
|
288
|
+
}
|
|
289
|
+
} else if constexpr (!is_block_argument_v<argument> &&
|
|
290
|
+
!is_optional_block_argument_v<argument>) {
|
|
291
|
+
const argument_spec& spec = specs_[spec_index];
|
|
292
|
+
VALUE selected = Qundef;
|
|
293
|
+
if (spec.keyword) {
|
|
294
|
+
selected = keyword_values[spec_index];
|
|
295
|
+
} else if (positional_index < positional_count) {
|
|
296
|
+
selected = argv[positional_index++];
|
|
297
|
+
}
|
|
298
|
+
if (selected == Qundef && spec.has_default) {
|
|
299
|
+
selected = spec.default_value.raw();
|
|
300
|
+
}
|
|
301
|
+
if (selected == Qundef) {
|
|
302
|
+
std::string message = "rbxx: missing argument ";
|
|
303
|
+
message += spec.name;
|
|
304
|
+
throw ruby_error(make_exception(rb_eArgError, message.c_str()));
|
|
305
|
+
}
|
|
306
|
+
parsed.slots[Index] = value{selected};
|
|
307
|
+
++spec_index;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
void read_block(parsed_arguments& parsed) const {
|
|
312
|
+
constexpr bool required = contains_required_block(std::make_index_sequence<arity>{});
|
|
313
|
+
if (rb_block_given_p()) {
|
|
314
|
+
parsed.block_value = value{protect(rb_block_proc)};
|
|
315
|
+
} else if (required) {
|
|
316
|
+
throw ruby_error(make_exception(rb_eArgError, "rbxx: required block was not given"));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
template <std::size_t... Index>
|
|
321
|
+
static consteval bool contains_required_block(std::index_sequence<Index...>) {
|
|
322
|
+
return (is_block_argument_v<std::tuple_element_t<Index, Tuple>> || ...);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
template <std::size_t... Index> static consteval bool has_rest(std::index_sequence<Index...>) {
|
|
326
|
+
return (is_rest_argument_v<std::tuple_element_t<Index, Tuple>> || ...);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
std::vector<argument_spec> specs_;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
template <typename Argument>
|
|
333
|
+
auto load_parsed_argument(const parsed_arguments& parsed, std::size_t index) {
|
|
334
|
+
if constexpr (is_block_argument_v<Argument>) {
|
|
335
|
+
return block{parsed.block_value};
|
|
336
|
+
} else if constexpr (is_optional_block_argument_v<Argument>) {
|
|
337
|
+
return parsed.block_value.is_nil() ? optional_block{} : optional_block{parsed.block_value};
|
|
338
|
+
} else if constexpr (is_rest_argument_v<Argument>) {
|
|
339
|
+
return args{parsed.rest};
|
|
340
|
+
} else {
|
|
341
|
+
return load_argument<Argument>(parsed.slots[index]);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
} // namespace detail
|
|
346
|
+
|
|
347
|
+
} // namespace rbxx
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <rbxx/data_object.hpp>
|
|
4
|
+
|
|
5
|
+
#include <array>
|
|
6
|
+
#include <functional>
|
|
7
|
+
#include <stdexcept>
|
|
8
|
+
#include <type_traits>
|
|
9
|
+
#include <utility>
|
|
10
|
+
|
|
11
|
+
namespace rbxx {
|
|
12
|
+
|
|
13
|
+
namespace detail {
|
|
14
|
+
|
|
15
|
+
template <typename Return, typename... Args>
|
|
16
|
+
struct is_non_bindable_class<std::function<Return(Args...)>> : std::true_type {};
|
|
17
|
+
|
|
18
|
+
inline void require_callback_gvl() {
|
|
19
|
+
#if defined(RBXX_DEBUG)
|
|
20
|
+
if (ruby_thread_has_gvl_p() == 0) {
|
|
21
|
+
throw std::runtime_error(
|
|
22
|
+
"rbxx: Ruby callback invoked without the GVL; reacquire it before calling the Proc");
|
|
23
|
+
}
|
|
24
|
+
#endif
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
} // namespace detail
|
|
28
|
+
|
|
29
|
+
/// @brief Converts a Ruby Proc into a GC-pinned C++ std::function.
|
|
30
|
+
template <typename Return, typename... Args> struct type_caster<std::function<Return(Args...)>> {
|
|
31
|
+
static constexpr std::string_view name = "Proc";
|
|
32
|
+
|
|
33
|
+
static std::function<Return(Args...)> load(value input) {
|
|
34
|
+
if (!matches(input)) {
|
|
35
|
+
detail::throw_type_error("Proc", input);
|
|
36
|
+
}
|
|
37
|
+
object callable{input};
|
|
38
|
+
return [callable = std::move(callable)](Args... args) -> Return {
|
|
39
|
+
detail::require_callback_gvl();
|
|
40
|
+
std::array<VALUE, sizeof...(Args)> converted{to_ruby(std::forward<Args>(args)).raw()...};
|
|
41
|
+
VALUE result = protect(rb_funcallv, callable.raw(), rb_intern("call"),
|
|
42
|
+
static_cast<int>(converted.size()), converted.data());
|
|
43
|
+
if constexpr (std::is_void_v<Return>) {
|
|
44
|
+
return;
|
|
45
|
+
} else {
|
|
46
|
+
return from_ruby<Return>(value{result});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static bool matches(value input) noexcept { return RTEST(rb_obj_is_proc(input.raw())); }
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
} // namespace rbxx
|