rcx 0.2.1 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a820a4875675677ecd561fdabdd91571147cba6e85942e763156f8456bbfd07d
4
- data.tar.gz: 20e63a83133047933f216487125292e919f76bcff9bd4c10ff917ff151651729
3
+ metadata.gz: 4074c5868fd711ee67726acd364d4aa243c5aeaf9fad6232756e9c1fde7539e3
4
+ data.tar.gz: e38a49e450466554a5845189cf2ae51ba31665f18389bf9a8d1a052fe6858353
5
5
  SHA512:
6
- metadata.gz: 828a90a3dc8fc8c79aa13ec72812df58f74e3c7dd4645b7ef25c6e58ac7fd76a831e29e2a6f3a6ef56717ceeb1ee89577eefa6436bc85b1a6343fdeb5d9e348b
7
- data.tar.gz: 6dcda5bb8368a7387c92ca9e1a36c5747be753c69a56805100b1017493bbc6c4c3d7bf62f9d382c42f274974ae5f7a50617c5dd54fc5a1b5f411b392857433a7
6
+ metadata.gz: cb13a7ca26f4caf50b3ad8038044f6abf7f932e612e1047df7c54bc543af07a4400a5f68fc64f37406eedff1234de7e99a58583a073c0cf4cbae916c0b03cdd8
7
+ data.tar.gz: 918833acbc1d6d1bc2b5c9678533ccd6a8bbdd8f5a6b2b87e18e0baf12443eb3887cb2b8b7ca5349f897e37eb9d0a2c06381e4501870db5e00c25317cc76a9f0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## UNRELEASED
2
2
 
3
+ ## v0.3.1 (2025-09-02)
4
+ - Now `extconf.rb`'s can only `require 'rcx/mkmf/c++20'` to enable RCX.
5
+
6
+ ## v0.3.0 (2025-08-22)
7
+ - Added `rcx::gvl::without_gvl`
8
+
3
9
  ## v0.2.1 (2025-07-10)
4
10
 
5
11
  ## v0.2.0 (2025-07-09)
data/README.md CHANGED
@@ -6,9 +6,7 @@ Write Ruby extensions in C++20. Inspired by [rice](https://github.com/ruby-rice/
6
6
  ## Creating a new extension
7
7
  In your `extconf.rb` file, add the following:
8
8
  ```ruby
9
- require 'mkmf
10
- $CXXFLAGS += ' -std=c++20' # or newer
11
- require 'rcx/mkmf'
9
+ require 'rcx/mkmf/c++20'
12
10
 
13
11
  create_header
14
12
  create_makefile('your_ext')
@@ -14,11 +14,11 @@
14
14
  #include <string>
15
15
  #include <string_view>
16
16
  #include <type_traits>
17
- #include <vector>
18
17
 
19
18
  #include <ruby.h>
20
19
  #include <ruby/encoding.h>
21
20
  #include <ruby/io/buffer.h>
21
+ #include <ruby/thread.h>
22
22
 
23
23
  #define rcx_assert(expr) assert((expr))
24
24
  #define rcx_delete(reason) delete
@@ -1520,6 +1520,82 @@ namespace rcx {
1520
1520
  /// @return Reference to the Ruby environment instance.
1521
1521
  static Ruby &get();
1522
1522
  };
1523
+
1524
+ /// Global VM Lock (GVL) management.
1525
+ ///
1526
+ namespace gvl {
1527
+ /// Flags for controlling GVL release behavior.
1528
+ ///
1529
+ enum class ReleaseFlags : int {
1530
+ /// Default behavior - allow interrupts and no special handling.
1531
+ None = 0,
1532
+ /// Prevent interrupt checking during execution.
1533
+ /// Use this when interrupts could cause problems with your function's execution.
1534
+ IntrFail = 1,
1535
+ /// The unblock function (if provided) is async-signal-safe.
1536
+ UbfAsyncSafe = 2,
1537
+ /// The function is safe to offload to a background thread or work pool.
1538
+ Offloadable = 4
1539
+ };
1540
+
1541
+ /// Bitwise OR operator for ReleaseFlags.
1542
+ constexpr ReleaseFlags operator|(ReleaseFlags lhs, ReleaseFlags rhs) noexcept {
1543
+ return static_cast<ReleaseFlags>(static_cast<int>(lhs) | static_cast<int>(rhs));
1544
+ }
1545
+
1546
+ /// Bitwise AND operator for ReleaseFlags.
1547
+ constexpr ReleaseFlags operator&(ReleaseFlags lhs, ReleaseFlags rhs) noexcept {
1548
+ return static_cast<ReleaseFlags>(static_cast<int>(lhs) & static_cast<int>(rhs));
1549
+ }
1550
+
1551
+ /// Releases the GVL and executes a function.
1552
+ ///
1553
+ /// This function releases the Global VM Lock (GVL) before executing the
1554
+ /// callback, allowing other Ruby threads to run concurrently.
1555
+ ///
1556
+ /// @warning The callback must not call any Ruby C API functions that may touch Ruby
1557
+ /// objects.
1558
+ ///
1559
+ /// @tparam F The type of the callback function.
1560
+ /// @tparam U The type of the unblock function.
1561
+ /// @param callback The function to execute without the GVL.
1562
+ /// @param ubf An optional unblock function to interrupt the callback
1563
+ /// execution. This function can be called from another thread.
1564
+ /// @param flags Control flags for the GVL release behavior.
1565
+ /// @return When the callback returns `void`, this function returns `true` if the callback
1566
+ /// was executed completely, or `false` if it was interrupted by `ubf`.
1567
+ /// When the callback returns a value, this function returns an `std::optional` containing
1568
+ /// the result if the callback was executed completely, or `std::nullopt`
1569
+ /// if it was interrupted.
1570
+ template <std::invocable<> F, std::invocable U>
1571
+ auto without_gvl(F callback, std::optional<U> ubf, ReleaseFlags flags) noexcept(noexcept(
1572
+ callback(), (*ubf)())) -> std::conditional_t<std::is_void_v<std::invoke_result_t<F>>, bool,
1573
+ std::optional<std::invoke_result_t<F>>>;
1574
+
1575
+ /// Releases the GVL and executes a function.
1576
+ ///
1577
+ /// This is an overload of `without_gvl` that does not take an unblock
1578
+ /// function.
1579
+ ///
1580
+ /// @warning The callback must not call any Ruby C API functions that may touch Ruby
1581
+ /// objects.
1582
+ ///
1583
+ /// @tparam F The type of the callback function.
1584
+ /// @param callback The function to execute without the GVL.
1585
+ /// @param flags Control flags for the GVL release behavior.
1586
+ /// @return When the callback returns `void`, this function returns `true` if the callback
1587
+ /// was executed completely, or `false` if it was interrupted.
1588
+ /// When the callback returns a value, this function returns an `std::optional` containing
1589
+ /// the result if the callback was executed completely, or `std::nullopt`
1590
+ /// if it was interrupted.
1591
+ template <std::invocable<> F>
1592
+ auto without_gvl(F &&callback, ReleaseFlags flags) noexcept(noexcept(callback()))
1593
+ -> std::conditional_t<std::is_void_v<std::invoke_result_t<F>>, bool,
1594
+ std::optional<std::invoke_result_t<F>>>;
1595
+
1596
+ /// Checks for pending interrupts.
1597
+ void check_interrupts();
1598
+ }
1523
1599
  }
1524
1600
 
1525
1601
  namespace std {
@@ -3,6 +3,7 @@
3
3
 
4
4
  #include <concepts>
5
5
  #include <memory>
6
+ #include <optional>
6
7
  #include <ranges>
7
8
  #include <stdexcept>
8
9
  #include <string_view>
@@ -10,6 +11,7 @@
10
11
  #include <type_traits>
11
12
  #include <typeinfo>
12
13
  #include <utility>
14
+ #include <variant>
13
15
 
14
16
  #include <ffi.h>
15
17
  #include <rcx/internal/rcx.hpp>
@@ -262,7 +264,7 @@ namespace rcx {
262
264
  return arg;
263
265
  }
264
266
 
265
- inline ArgSplat::ResultType ArgSplat::parse(Ruby &, Value self, std::span<Value> &args) {
267
+ inline ArgSplat::ResultType ArgSplat::parse(Ruby &, Value, std::span<Value> &args) {
266
268
  auto result = Array::new_from(args);
267
269
  args = {};
268
270
  return result;
@@ -1414,4 +1416,108 @@ namespace rcx {
1414
1416
  }
1415
1417
  }
1416
1418
  }
1419
+
1420
+ namespace gvl {
1421
+ template <std::invocable<> F, std::invocable<> U>
1422
+ auto without_gvl(F callback, std::optional<U> ubf, ReleaseFlags flags) noexcept(noexcept(
1423
+ callback(), (*ubf)())) -> std::conditional_t<std::is_void_v<std::invoke_result_t<F>>, bool,
1424
+ std::optional<std::invoke_result_t<F>>> {
1425
+
1426
+ using ResultType = std::conditional_t<std::is_void_v<std::invoke_result_t<F>>, std::monostate,
1427
+ std::optional<std::invoke_result_t<F>>>;
1428
+
1429
+ struct CallbackData {
1430
+ F callback;
1431
+ [[no_unique_address]] ResultType result;
1432
+ std::exception_ptr exception;
1433
+ };
1434
+
1435
+ struct UbfData {
1436
+ U ubf;
1437
+ std::exception_ptr exception;
1438
+ };
1439
+
1440
+ CallbackData data{std::move(callback), ResultType{}, nullptr};
1441
+ std::optional<UbfData> ubf_data;
1442
+ if(ubf) {
1443
+ ubf_data.emplace(std::move(*ubf), nullptr);
1444
+ }
1445
+
1446
+ auto callback_wrapper = [](void *RCX_Nonnull arg) -> void * {
1447
+ auto &data = *static_cast<CallbackData * RCX_Nonnull>(arg);
1448
+ try {
1449
+ if constexpr(std::is_void_v<std::invoke_result_t<F>>) {
1450
+ data.callback();
1451
+ } else {
1452
+ data.result = data.callback();
1453
+ }
1454
+ } catch(...) {
1455
+ data.exception = std::current_exception();
1456
+ }
1457
+ return reinterpret_cast<void *>(1); // Non-null to indicate execution
1458
+ };
1459
+
1460
+ using Ubf = void (*RCX_Nullable)(void *RCX_Nonnull);
1461
+ Ubf ubf_wrapper = nullptr;
1462
+ if(ubf_data) {
1463
+ ubf_wrapper = [](void *RCX_Nonnull arg) -> void {
1464
+ auto &data = *static_cast<UbfData * RCX_Nonnull>(arg);
1465
+ try {
1466
+ data.ubf();
1467
+ } catch(...) {
1468
+ data.exception = std::current_exception();
1469
+ }
1470
+ };
1471
+ }
1472
+
1473
+ void *result = rb_nogvl(callback_wrapper, &data, ubf_wrapper,
1474
+ ubf_data ? std::addressof(*ubf_data) : nullptr, static_cast<int>(flags));
1475
+
1476
+ // Check for UBF exceptions first. The callback was cancelled with UBF, which then raised.
1477
+ if(ubf_data && ubf_data->exception) {
1478
+ std::rethrow_exception(ubf_data->exception);
1479
+ }
1480
+
1481
+ // If rb_nogvl returned nullptr, the callback was cancelled.
1482
+ if(result == nullptr) {
1483
+ if constexpr(std::is_void_v<std::invoke_result_t<F>>) {
1484
+ return false;
1485
+ } else {
1486
+ return std::nullopt;
1487
+ }
1488
+ }
1489
+
1490
+ // Re-throw any exception that occurred in the callback.
1491
+ if(data.exception) {
1492
+ std::rethrow_exception(data.exception);
1493
+ }
1494
+
1495
+ // Return the callback result.
1496
+ if constexpr(std::is_void_v<std::invoke_result_t<F>>) {
1497
+ return true;
1498
+ } else {
1499
+ return std::move(data.result);
1500
+ }
1501
+ }
1502
+
1503
+ template <std::invocable<> F, std::invocable<> U>
1504
+ auto without_gvl(F &&callback, U ubf, ReleaseFlags flags) noexcept(noexcept(callback(), ubf()))
1505
+ -> std::conditional_t<std::is_void_v<std::invoke_result_t<F>>, bool,
1506
+ std::optional<std::invoke_result_t<F>>> {
1507
+ return without_gvl(
1508
+ std::forward<F>(callback), std::optional<std::remove_cvref_t<U>>(std::move(ubf)), flags);
1509
+ }
1510
+
1511
+ template <std::invocable<> F>
1512
+ auto without_gvl(F &&callback, ReleaseFlags flags) noexcept(noexcept(callback()))
1513
+ -> std::conditional_t<std::is_void_v<std::invoke_result_t<F>>, bool,
1514
+ std::optional<std::invoke_result_t<F>>> {
1515
+ using DefaultUbf = void (*)();
1516
+ return without_gvl(std::forward<F>(callback), std::optional<DefaultUbf>(std::nullopt), flags);
1517
+ }
1518
+
1519
+ inline void check_interrupts() {
1520
+ detail::protect([]() noexcept { ::rb_thread_check_ints(); });
1521
+ }
1522
+ }
1417
1523
  }
@@ -0,0 +1,3 @@
1
+ require_relative '../mkmf'
2
+ include RCX::MakeMakefile
3
+ setup_rcx(cxx_standard: 'c++20')
data/lib/rcx/mkmf.rb CHANGED
@@ -1,56 +1,76 @@
1
1
  # SPDX-License-Identifier: BSL-1.0
2
2
  # SPDX-FileCopyrightText: Copyright 2024-2025 Kasumi Hanazuki <kasumi@rollingapple.net>
3
3
  require 'mkmf'
4
- include MakeMakefile['C++']
4
+ require_relative '../rcx'
5
5
 
6
- root = File.join(__dir__, '../..')
6
+ module RCX
7
+ CXX_STANDARD_FLAGS = {
8
+ 'c++20' => %w[--std=c++20 --std=c++2a].freeze,
9
+ 'c++23' => %w[--std=c++23 --std=c++2b].freeze,
10
+ 'c++26' => %w[--std=c++26 --std=c++2c].freeze,
11
+ }.freeze
7
12
 
8
- $INCFLAGS << " -I#{File.join(root, 'include').shellescape}"
13
+ root = File.join(__dir__, '../..')
14
+ INCDIR = File.join(root, 'include').shellescape
15
+ HEADERS = Dir[File.join(root, 'include/**/*.hpp')]
9
16
 
10
- $rcx_headers = Dir[File.join(root, 'include/**/*.hpp')]
17
+ module MakeMakefile
18
+ include ::MakeMakefile['C++']
11
19
 
12
- include (Module.new do
13
- def configuration(...)
14
- super.tap do |mk|
15
- mk << <<MAKEFILE
16
- rcx_headers = #{$rcx_headers.join(?\s)}
17
- ruby_headers := $(ruby_headers) $(rcx_headers)
18
- MAKEFILE
19
- end
20
- end
21
- end)
20
+ def setup_rcx(cxx_standard: 'c++20')
21
+ CXX_STANDARD_FLAGS.fetch(cxx_standard).find do |flag|
22
+ if checking_for("whether #{flag} is accepted as CXXFLAGS") { try_cflags(flag) }
23
+ $CXXFLAGS << " " << flag
24
+ true
25
+ else
26
+ false
27
+ end
28
+ end or raise "C++ compiler does not support #{cxx_standard}"
22
29
 
23
- ## libffi
30
+ $INCFLAGS << " -I#{INCDIR}"
24
31
 
25
- dir_config('libffi').any? || pkg_config('libffi')
26
- ffi_h = 'ffi.h'
27
- unless have_func('ffi_prep_cif', ffi_h)
28
- raise "libffi was not found"
29
- end
30
- unless have_func('ffi_closure_alloc', ffi_h) && have_func('ffi_prep_closure_loc', ffi_h)
31
- raise "libffi does not support closures"
32
- end
32
+ ## libffi
33
+ dir_config('libffi').any? || pkg_config('libffi')
34
+ ffi_h = 'ffi.h'
35
+ unless have_func('ffi_prep_cif', ffi_h)
36
+ raise "libffi was not found"
37
+ end
38
+ unless have_func('ffi_closure_alloc', ffi_h) && have_func('ffi_prep_closure_loc', ffi_h)
39
+ raise "libffi does not support closures"
40
+ end
33
41
 
34
- if have_header('cxxabi.h')
35
- have_func('abi::__cxa_demangle', 'cxxabi.h')
36
- have_func('abi::__cxa_current_exception_type', 'cxxabi.h')
37
- end
42
+ if have_header('cxxabi.h')
43
+ have_func('abi::__cxa_demangle', 'cxxabi.h')
44
+ have_func('abi::__cxa_current_exception_type', 'cxxabi.h')
45
+ end
38
46
 
39
- if checking_for("std::is_layout_compatible<>") {
40
- try_compile(<<'CXX')
47
+ if checking_for("std::is_layout_compatible<>") {
48
+ try_compile(<<'CXX')
41
49
  #include <type_traits>
42
50
  struct A { int a; };
43
51
  struct B { int b; };
44
52
  static_assert(std::is_layout_compatible<A, B>::value);
45
53
  CXX
46
- }
47
- $defs.push("-DHAVE_STD_IS_LAYOUT_COMPATIBLE=1")
48
- end
54
+ }
55
+ $defs.push("-DHAVE_STD_IS_LAYOUT_COMPATIBLE=1")
56
+ end
49
57
 
50
- if checking_for("nullability extension") {
51
- try_compile("void *_Nullable p, *_Nonnull q;")
52
- }
53
- $defs.push("-DHAVE_FEATURE_NULLABILITY=1")
54
- end
58
+ if checking_for("nullability extension") {
59
+ try_compile("void *_Nullable p, *_Nonnull q;")
60
+ }
61
+ $defs.push("-DHAVE_FEATURE_NULLABILITY=1")
62
+ end
55
63
 
56
- have_func('ruby_thread_has_gvl_p', 'ruby/thread.h')
64
+ have_func('ruby_thread_has_gvl_p', 'ruby/thread.h')
65
+ end
66
+
67
+ def configuration(...)
68
+ super.tap do |mk|
69
+ mk << <<MAKEFILE
70
+ rcx_headers = #{RCX::HEADERS.join(?\s)}
71
+ ruby_headers := $(ruby_headers) $(rcx_headers)
72
+ MAKEFILE
73
+ end
74
+ end
75
+ end
76
+ end
data/lib/rcx/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RCX
2
- VERSION = -'0.2.1'
2
+ VERSION = -'0.3.1'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rcx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kasumi Hanazuki
@@ -44,6 +44,7 @@ files:
44
44
  - include/rcx/rcx.hpp
45
45
  - lib/rcx.rb
46
46
  - lib/rcx/mkmf.rb
47
+ - lib/rcx/mkmf/c++20.rb
47
48
  - lib/rcx/version.rb
48
49
  - package.json
49
50
  - yarn.lock