From 535adcdedf3b285361575da4178c46977d9f26f7 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 25 Feb 2024 00:52:47 -0800 Subject: [PATCH] Add std::string_view support. See #196 --- .gitignore | 3 +- doc/stl.rst | 1 + doc/stl/string_view.rst | 9 ++++ rice/cpp_api/String.hpp | 3 ++ rice/cpp_api/String.ipp | 4 ++ rice/stl.hpp | 1 + rice/stl/string_view.hpp | 6 +++ rice/stl/string_view.ipp | 69 +++++++++++++++++++++++++++ test/CMakeLists.txt | 1 + test/test_Stl_String_View.cpp | 88 +++++++++++++++++++++++++++++++++++ test/test_String.cpp | 15 ++++++ 11 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 doc/stl/string_view.rst create mode 100644 rice/stl/string_view.hpp create mode 100644 rice/stl/string_view.ipp create mode 100644 test/test_Stl_String_View.cpp diff --git a/.gitignore b/.gitignore index 2ddcd517..b8b73f76 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ Gemfile.lock *.bundle README.doxygen doc/_build - +.vs/ +cmake* pkg *.log .*.swp diff --git a/doc/stl.rst b/doc/stl.rst index 98e4a099..a237735e 100644 --- a/doc/stl.rst +++ b/doc/stl.rst @@ -13,6 +13,7 @@ STL stl/reference_wrapper stl/smart_pointers stl/string + stl/string_view stl/unordered_map stl/variant stl/vector diff --git a/doc/stl/string_view.rst b/doc/stl/string_view.rst new file mode 100644 index 00000000..5b407972 --- /dev/null +++ b/doc/stl/string_view.rst @@ -0,0 +1,9 @@ +.. _std_string_view: + +std::string_view +----------------- +``std::string_view`` is a read-only reference to a sequence of `char`s. It provides a way of passing strings without the overhead of copying `std::string`. + +On input, Rice treats ``std::string_view`` as a Builtin type which means it copies the portion of the ``char`` sequence that a ``std::string_view`` references in C++ to Ruby. Please refer to the :ref:`_std_string_view` documentation to learn about how Rice handles encodings.my-refere + +On output, Rice creates a ``std::string_view`` that references a Ruby string's underlying `char` buffer. Note this is DANGEROUS. Sooner or later the Ruby string will be garbage collected or moved as part of compaction, thus invalidating the `char` buffer. So use this with caution. \ No newline at end of file diff --git a/rice/cpp_api/String.hpp b/rice/cpp_api/String.hpp index 662b0f20..57a79d44 100644 --- a/rice/cpp_api/String.hpp +++ b/rice/cpp_api/String.hpp @@ -38,6 +38,9 @@ namespace Rice //! Construct a String from an std::string. String(std::string const& s); + //! Construct a String from an std::string_view. + String(std::string_view const& s); + //! Format a string using printf-style formatting. template static inline String format(char const* fmt, Arg_Ts&&...args); diff --git a/rice/cpp_api/String.ipp b/rice/cpp_api/String.ipp index d54b806d..a0297a54 100644 --- a/rice/cpp_api/String.ipp +++ b/rice/cpp_api/String.ipp @@ -20,6 +20,10 @@ namespace Rice { } + inline String::String(std::string_view const& s) : Builtin_Object(detail::protect(rb_str_new, s.data(), (long)s.length())) + { + } + inline String::String(Identifier id) : Builtin_Object(detail::protect(rb_str_new2, id.c_str())) { } diff --git a/rice/stl.hpp b/rice/stl.hpp index 707bbdf4..97833884 100644 --- a/rice/stl.hpp +++ b/rice/stl.hpp @@ -2,6 +2,7 @@ #define Rice__stl__hpp_ #include "stl/string.hpp" +#include "stl/string_view.hpp" #include "stl/complex.hpp" #include "stl/optional.hpp" #include "stl/reference_wrapper.hpp" diff --git a/rice/stl/string_view.hpp b/rice/stl/string_view.hpp new file mode 100644 index 00000000..b34d33f3 --- /dev/null +++ b/rice/stl/string_view.hpp @@ -0,0 +1,6 @@ +#ifndef Rice__stl__string_view__hpp_ +#define Rice__stl__string_view__hpp_ + +#include "string_view.ipp" + +#endif // Rice__stl__string_view__hpp_ \ No newline at end of file diff --git a/rice/stl/string_view.ipp b/rice/stl/string_view.ipp new file mode 100644 index 00000000..9120490c --- /dev/null +++ b/rice/stl/string_view.ipp @@ -0,0 +1,69 @@ +#include +#include "../detail/ruby.hpp" +#include "../detail/Type.hpp" +#include "../detail/from_ruby.hpp" +#include "../detail/to_ruby.hpp" + +namespace Rice::detail +{ + template<> + struct Type + { + static bool verify() + { + return true; + } + }; + + template<> + class To_Ruby + { + public: + VALUE convert(std::string_view const& x) + { + return detail::protect(rb_external_str_new, x.data(), (long)x.size()); + } + }; + + template<> + class To_Ruby + { + public: + VALUE convert(std::string_view const& x) + { + return detail::protect(rb_external_str_new, x.data(), (long)x.size()); + } + }; + + template<> + class From_Ruby + { + public: + From_Ruby() = default; + + explicit From_Ruby(Arg* arg) : arg_(arg) + { + } + + bool is_convertible(VALUE value) + { + return rb_type(value) == RUBY_T_STRING; + } + + std::string_view convert(VALUE value) + { + if (value == Qnil && this->arg_ && this->arg_->hasDefaultValue()) + { + return this->arg_->defaultValue(); + } + else + { + detail::protect(rb_check_type, value, (int)T_STRING); + return std::string_view(RSTRING_PTR(value), RSTRING_LEN(value)); + } + } + + private: + Arg* arg_ = nullptr; + }; +} \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 36dd019f..d07231e4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -41,6 +41,7 @@ add_executable (unittest "test_Stl_Reference_Wrapper.cpp" "test_Stl_SmartPointer.cpp" "test_Stl_String.cpp" + "test_Stl_String_View.cpp" "test_Stl_Unordered_Map.cpp" "test_Stl_Variant.cpp" "test_Stl_Vector.cpp" diff --git a/test/test_Stl_String_View.cpp b/test/test_Stl_String_View.cpp new file mode 100644 index 00000000..0bf5ecce --- /dev/null +++ b/test/test_Stl_String_View.cpp @@ -0,0 +1,88 @@ +#include "unittest.hpp" +#include "embed_ruby.hpp" +#include +#include +#include + +#include + +using namespace Rice; + +TESTSUITE(StlStringView); + +SETUP(StlStringView) +{ + embed_ruby(); +} + +TESTCASE(std_string_view_to_ruby) +{ + ASSERT(rb_equal(String("").value(), detail::to_ruby(std::string_view("")))); + ASSERT(rb_equal(String("foo").value(), detail::to_ruby(std::string_view("foo")))); +} + +TESTCASE(std_string_view_to_ruby_encoding) +{ + VALUE value = detail::to_ruby(std::string_view("Some String")); + String string(value); + Object encoding = string.call("encoding"); + String encodingName = encoding.call("name"); + std::string_view result = detail::From_Ruby().convert(encodingName); + if(result != "ASCII-8BIT" && result != "US-ASCII" && result != "UTF-8") { + FAIL("Encoding incorrect", "ASCII-8BIT, US-ASCII, or UTF-8 (Windows)", result); + } +} + +TESTCASE(std_string_view_to_ruby_encoding_utf8) +{ + rb_encoding* defaultEncoding = rb_default_external_encoding(); + + VALUE utf8Encoding = rb_enc_from_encoding(rb_utf8_encoding()); + rb_enc_set_default_external(utf8Encoding); + + VALUE value = detail::to_ruby(std::string_view("Some String")); + Object object(value); + Object encoding = object.call("encoding"); + Object encodingName = encoding.call("name"); + ASSERT_EQUAL("UTF-8", detail::From_Ruby().convert(encodingName)); + + rb_enc_set_default_external(rb_enc_from_encoding(defaultEncoding)); +} + +TESTCASE(std_string_view_from_ruby) +{ + ASSERT_EQUAL(std::string_view(""), detail::From_Ruby().convert(rb_str_new2(""))); + ASSERT_EQUAL(std::string_view("foo"), detail::From_Ruby().convert(rb_str_new2("foo"))); + + ASSERT_EXCEPTION_CHECK( + Exception, + detail::From_Ruby().convert(rb_float_new(15.512)), + ASSERT_EQUAL("wrong argument type Float (expected String)", ex.what()) + ); +} + +TESTCASE(std_string_view_to_ruby_with_binary) +{ + Rice::String got = detail::to_ruby(std::string_view("\000test", 5)); + + ASSERT_EQUAL(String(std::string_view("\000test", 5)), got); + ASSERT_EQUAL(5ul, got.length()); +} + +TESTCASE(std_string_view_from_ruby_with_binary) +{ + std::string_view got = detail::From_Ruby().convert(rb_str_new("\000test", 5)); + ASSERT_EQUAL(5ul, got.length()); + ASSERT_EQUAL(std::string_view("\000test", 5), got); +} + +TESTCASE(std_string_view_from_ruby_refefence) +{ + VALUE value = rb_str_new("test", 4); + std::string_view view = detail::From_Ruby().convert(value); + ASSERT_EQUAL("test", view); + + String string(value); + string.instance_eval("self[1] = 'a'"); + ASSERT_EQUAL("tast", view); +} diff --git a/test/test_String.cpp b/test/test_String.cpp index 0fb4221d..df32ff11 100644 --- a/test/test_String.cpp +++ b/test/test_String.cpp @@ -54,6 +54,21 @@ TESTCASE(construct_from_std_string) ASSERT_EQUAL("foo", RSTRING_PTR(s.value())); } +TESTCASE(construct_from_std_string_view) +{ + std::string_view foo("foo"); + String s(foo); + ASSERT_EQUAL(T_STRING, rb_type(s)); + ASSERT_EQUAL("foo", RSTRING_PTR(s.value())); + + { + foo = "foo 2"; + s = foo; + } + ASSERT_EQUAL(T_STRING, rb_type(s)); + ASSERT_EQUAL("foo 2", RSTRING_PTR(s.value())); +} + TESTCASE(format) { String s(String::format("%s %d", "foo", 42));