Skip to content
Merged

Dev #396

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 53 additions & 5 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ on:
branches: [ master ]

jobs:
tests:
mkmf:
name: mkmf (${{ matrix.os }}, ${{ matrix.ruby }}, C++17)
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-2025]
ruby: ['3.2', '3.3', '3.4', '4.0']
include:
- os: ubuntu-22.04
ruby: '3.2'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
Expand All @@ -33,4 +31,54 @@ jobs:
if: always()
with:
name: mkmf-${{ matrix.os }}-${{ matrix.version }}-${{ matrix.ruby }}
path: test/mkmf.log
path: test/mkmf.log

cmake:
name: cmake (${{ matrix.os }}, C++${{ matrix.cpp_standard }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-2025]
cpp_standard: ['17', '20', '23']
include:
- os: ubuntu-latest
preset: linux-debug
ruby: '4.0'
- os: macos-latest
preset: macos-debug
ruby: '4.0'
- os: windows-2025
preset: msvc-debug
ruby: mswin

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}

- if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libffi-dev

- if: runner.os == 'macOS'
run: brew install libffi

- if: runner.os == 'Windows'
uses: ilammy/msvc-dev-cmd@v1

- if: runner.os == 'Windows'
uses: seanmiddleditch/gha-setup-ninja@v6

- if: runner.os == 'Windows'
shell: pwsh
run: |
git clone https://github.com/microsoft/vcpkg.git "$env:RUNNER_TEMP\vcpkg"
& "$env:RUNNER_TEMP\vcpkg\bootstrap-vcpkg.bat"
echo "VCPKG_ROOT=$env:RUNNER_TEMP\vcpkg" >> $env:GITHUB_ENV
& "$env:RUNNER_TEMP\vcpkg\vcpkg.exe" install libffi:x64-windows

- run: cmake --preset ${{ matrix.preset }} -DCMAKE_CXX_STANDARD=${{ matrix.cpp_standard }}
- run: cmake --build --preset ${{ matrix.preset }}
4 changes: 4 additions & 0 deletions Agents.md → AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ Run specific test suites by passing suite names as arguments:
```bash
./build/linux-debug/test/unittest Array Hash Iterator
```

## Quick Compile Testing

To quickly test if a single test file compiles, temporarily edit `test/CMakeLists.txt` to include only that file (plus `unittest.cpp` and `embed_ruby.cpp`). Revert the change when done.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Changelog

## 4.11.3 (?)
## 4.11.3 (2026-02-25)

### Enhancements

* Add support for `std::shared_ptr<const T>`
* Add support for `std::unique_ptr<const T>`

* Add support for non-constructible objects in std::pair, std::map, std::multimap
* Add support for wrapping pointers to abstract types that cannot be created nor destroyed
* Fix converting std::nullptr_t to Ruby

## 4.11.2 (2026-02-21)

Expand Down
6 changes: 3 additions & 3 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"toolchainFile": "$env{VCPKG_ROOT}\\scripts\\buildsystems\\vcpkg.cmake",
"cacheVariables": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE"
"CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /Zc:__cplusplus /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE"
},
"condition": {
"type": "equals",
Expand Down Expand Up @@ -155,7 +155,7 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_COMPILER": "clang-cl.exe",
"CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE /clang:-Wno-unused-private-field",
"CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /Zc:__cplusplus /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE /clang:-Wno-unused-private-field",
"CMAKE_CXX_FLAGS_DEBUG": "/Od /Zi"
}
},
Expand All @@ -166,7 +166,7 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_CXX_COMPILER": "clang-cl.exe",
"CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE /clang:-Wno-unused-private-field",
"CMAKE_CXX_FLAGS": "/EHs /W4 /bigobj /utf-8 /Zc:__cplusplus /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE /clang:-Wno-unused-private-field",
"CMAKE_CXX_FLAGS_RELEASE": "/O2 /DNDEBUG",
"CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON"
}
Expand Down
6 changes: 5 additions & 1 deletion docs/packaging/build_settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ For MINGW:
For Microsoft Visual C++ and Windows Clang:

```bash
/std:c++17 /EHs /permissive- /bigobj /utf-8 -D_ALLOW_KEYWORD_MACROS -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE
/std:c++17 /EHs /permissive- /bigobj /utf-8 /Zc:__cplusplus -D_ALLOW_KEYWORD_MACROS -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE
```

These options are described below:
Expand All @@ -36,6 +36,10 @@ Second, bigobject support needs to enabled. This tells the compiler to increase

Rice uses UTF-8 characters when [mapping](../stl/stl.md#automatically-generated-ruby-classes) instantiated STL templates to Ruby class names.

### __cplusplus Macro

By default, MSVC does not update the `__cplusplus` preprocessor macro to reflect the actual C++ standard in use — it always reports `199711L`. The `/Zc:__cplusplus` flag fixes this so that Rice's `#if __cplusplus` checks work correctly.

### Exception Handling Model

For Visual C++, the default exception [model](https://learn.microsoft.com/en-us/cpp/build/reference/eh-exception-handling-model?view=msvc-170) setting of `/EHsc` crashes Ruby when calling longjmp with optimizations enabled (/O2). Therefore you must `/EHs` instead.
Expand Down
26 changes: 6 additions & 20 deletions include/rice/rice.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8854,31 +8854,15 @@ namespace Rice::detail
}
}

void* convert(VALUE value)
std::nullptr_t convert(VALUE value)
{
if (value == Qnil)
{
return nullptr;
}

if (this->arg_ && this->arg_->isOpaque())
{
return (void*)value;
}

switch (rb_type(value))
{
case RUBY_T_NIL:
{
return nullptr;
break;
}
default:
{
throw Exception(rb_eTypeError, "wrong argument type %s (expected %s)",
detail::protect(rb_obj_classname, value), "nil");
}
}
throw Exception(rb_eTypeError, "wrong argument type %s (expected %s)",
detail::protect(rb_obj_classname, value), "nil");
}
private:
Arg* arg_ = nullptr;
Expand Down Expand Up @@ -10277,7 +10261,9 @@ namespace Rice::detail

if constexpr (is_complete_v<T>)
{
if constexpr (std::is_destructible_v<T>)
// is_abstract_v requires a complete type, so nest inside is_complete_v.
// Deleting an abstract class through a non-virtual destructor is UB.
if constexpr (std::is_destructible_v<T> && !std::is_abstract_v<T>)
{
if (this->isOwner_)
{
Expand Down
26 changes: 17 additions & 9 deletions include/rice/stl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1310,8 +1310,12 @@ namespace Rice
private:
void define_constructors()
{
klass_.define_constructor(Constructor<T>())
.define_constructor(Constructor<T, First_Parameter_T, Second_Parameter_T>(), Arg("x").keepAlive(), Arg("y").keepAlive());
if constexpr (std::is_default_constructible_v<T>)
{
klass_.define_constructor(Constructor<T>());
}

klass_.define_constructor(Constructor<T, First_Parameter_T, Second_Parameter_T>(), Arg("x").keepAlive(), Arg("y").keepAlive());

if constexpr (std::is_copy_constructible_v<First_T> && std::is_copy_constructible_v<Second_T>)
{
Expand Down Expand Up @@ -1650,7 +1654,7 @@ namespace Rice
}, Arg("key"))
.define_method("[]=", [](T& map, Key_T key, Mapped_Parameter_T value) -> Mapped_T
{
map[key] = value;
map.insert_or_assign(key, value);
return value;
}, Arg("key").keepAlive(), Arg("value").keepAlive());

Expand Down Expand Up @@ -1772,7 +1776,7 @@ namespace Rice
// exceptions propogate back to Ruby
return cpp_protect([&]
{
result->operator[](From_Ruby<T>().convert(key)) = From_Ruby<U>().convert(value);
result->insert_or_assign(From_Ruby<T>().convert(key), From_Ruby<U>().convert(value));
return ST_CONTINUE;
});
}
Expand Down Expand Up @@ -3201,7 +3205,11 @@ namespace Rice

if constexpr (detail::is_complete_v<T> && !std::is_void_v<T>)
{
result.define_constructor(Constructor<SharedPtr_T, typename SharedPtr_T::element_type*>(), Arg("value").takeOwnership());
// is_abstract_v requires a complete type, so it must be nested inside the is_complete_v check
if constexpr (!std::is_abstract_v<T>)
{
result.define_constructor(Constructor<SharedPtr_T, typename SharedPtr_T::element_type*>(), Arg("value").takeOwnership());
}
}

// Forward methods to wrapped T
Expand Down Expand Up @@ -3256,7 +3264,7 @@ namespace Rice::detail
}
else if (rb_typeddata_inherited_p(this->inner_rb_data_type_, requestedType))
{
return this->data_.get();
return (void*)this->data_.get();
}
else
{
Expand Down Expand Up @@ -3931,7 +3939,7 @@ namespace Rice::detail
}
else if (rb_typeddata_inherited_p(this->inner_rb_data_type_, requestedType))
{
return this->data_.get();
return (void*)this->data_.get();
}
else
{
Expand Down Expand Up @@ -4171,7 +4179,7 @@ namespace Rice
}, Arg("key"))
.define_method("[]=", [](T& unordered_map, Key_T key, Mapped_Parameter_T value) -> Mapped_T
{
unordered_map[key] = value;
unordered_map.insert_or_assign(key, value);
return value;
}, Arg("key").keepAlive(), Arg("value").keepAlive());

Expand Down Expand Up @@ -4293,7 +4301,7 @@ namespace Rice
// exceptions propogate back to Ruby
return cpp_protect([&]
{
result->operator[](From_Ruby<T>().convert(key)) = From_Ruby<U>().convert(value);
result->insert_or_assign(From_Ruby<T>().convert(key), From_Ruby<U>().convert(value));
return ST_CONTINUE;
});
}
Expand Down
10 changes: 6 additions & 4 deletions lib/mkmf-rice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ def cpp_command(outfile, opt="")
# Now pull in the C++ support
include MakeMakefile['C++']

# Rice needs c++17.
# Rice needs c++17 or higher. Use --with-cxx-standard=20 to override.
std = with_config('cxx-standard', '17')

if IS_MSWIN
$CXXFLAGS += " /std:c++17 /EHs /permissive- /bigobj /utf-8"
$CXXFLAGS += " /std:c++#{std} /EHs /permissive- /bigobj /utf-8 /Zc:__cplusplus"
$CPPFLAGS += " -D_ALLOW_KEYWORD_MACROS -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE"
elsif IS_MINGW
$CXXFLAGS += " -std=c++17 -Wa,-mbig-obj"
$CXXFLAGS += " -std=c++#{std} -Wa,-mbig-obj"
else
$CXXFLAGS += " -std=c++17"
$CXXFLAGS += " -std=c++#{std}"
end

# Rice needs to include its header. Let's setup the include path
Expand Down
2 changes: 1 addition & 1 deletion lib/rice/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Rice
VERSION = "4.11.2"
VERSION = "4.11.3"
end
6 changes: 5 additions & 1 deletion rice/stl/ostream.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ namespace Rice::stl

void define_methods()
{
#if __cplusplus >= 202002L
klass_.define_method<std::string(std::ostringstream::*)() const&>("str", &std::ostringstream::str)
#else
klass_.define_method<std::string(std::ostringstream::*)() const>("str", &std::ostringstream::str)
.define_method("str=", [](std::ostringstream& stream, const std::string& s) { stream.str(s); }, Arg("str"));
#endif
.define_method("str=", [](std::ostringstream& stream, const std::string& s) { stream.str(s); }, Arg("str"));

rb_define_alias(klass_, "to_s", "str");
}
Expand Down
21 changes: 21 additions & 0 deletions rice/traits/function_traits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,27 @@ namespace Rice::detail
{
};

// ref-qualified member Functions on C++ classes (C++20 uses these for std library types)
template<typename Return_T, typename Class_T, typename...Parameter_Ts>
struct function_traits<Return_T(Class_T::*)(Parameter_Ts...) &> : public function_traits<Return_T(Class_T*, Parameter_Ts...)>
{
};

template<typename Return_T, typename Class_T, typename...Parameter_Ts>
struct function_traits<Return_T(Class_T::*)(Parameter_Ts...) const&> : public function_traits<Return_T(Class_T*, Parameter_Ts...)>
{
};

template<typename Return_T, typename Class_T, typename...Parameter_Ts>
struct function_traits<Return_T(Class_T::*)(Parameter_Ts...) & noexcept> : public function_traits<Return_T(Class_T*, Parameter_Ts...)>
{
};

template<typename Return_T, typename Class_T, typename...Parameter_Ts>
struct function_traits<Return_T(Class_T::*)(Parameter_Ts...) const& noexcept> : public function_traits<Return_T(Class_T*, Parameter_Ts...)>
{
};

/*// Functors and lambdas
template<class Function_T>
struct function_traits<Function_T&> : public function_traits<Function_T>
Expand Down
31 changes: 31 additions & 0 deletions test/test_Attribute.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -590,4 +590,35 @@ TESTCASE(KeepAlive)
// This should work because keepAlive prevents MyClass2 from being GC'd
ASSERT_NOT_EQUAL(nullptr, dataStruct->myClass2);
ASSERT_EQUAL(43, dataStruct->myClass2->value);
}

namespace
{
struct FuncPtrStruct
{
int (*callback)(int) = nullptr;
};
}

TESTCASE(function_pointer_attribute)
{
Module m = define_module("Testing");

Class c = define_class<FuncPtrStruct>("FuncPtrStruct")
.define_constructor(Constructor<FuncPtrStruct>())
.define_attr("callback", &FuncPtrStruct::callback);

Object o = c.call("new");

// Set the callback via Ruby using a lambda
std::string code = R"(struct = FuncPtrStruct.new
struct.callback = lambda { |x| x * 2 }
struct)";

Data_Object<FuncPtrStruct> funcPtrStruct = m.module_eval(code);

// Invoke the callback from C++ to verify it works
ASSERT_NOT_EQUAL(nullptr, funcPtrStruct->callback);
int result = funcPtrStruct->callback(5);
ASSERT_EQUAL(10, result);
}
6 changes: 2 additions & 4 deletions test/test_Callback.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,8 @@ TESTCASE(LambdaCallBack)
ASSERT((globalCallback != nullptr));

int ref = 4;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wwrite-strings"
char* result = triggerCallback(1, 2, true, "hello", ref);
#pragma GCC diagnostic pop
char hello[] = "hello";
char* result = triggerCallback(1, 2, true, hello, ref);
ASSERT_EQUAL("1 - 2.0 - true - hello - 4", result);
}

Expand Down
Loading