diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 517b6dab..8f9285e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,6 +58,24 @@ jobs: -o /tmp/hr-subset /tmp/hr-subset harfrust/tests/fonts/rb_custom/PT_Sans-Caption-Web-Regular.ttf + - name: HarfBuzz C++ ownership helpers + run: | + for standard in c++11 c++17; do + for order in CORE SUBSET_FIRST SUBSET_LAST; do + library_directory=target/debug + if [ "$order" = CORE ]; then + library_directory=target/capi-default/debug + fi + c++ -std="$standard" -pedantic-errors -Wall -Wextra -Werror \ + -D"HR_TEST_$order" \ + -Iharfrust_capi/include \ + harfrust_capi/tests/cplusplus.cc -L"$library_directory" \ + -lharfrust_c -Wl,-rpath,"$PWD/$library_directory" \ + -o /tmp/hr-cplusplus + /tmp/hr-cplusplus harfrust/tests/fonts/rb_custom/PT_Sans-Caption-Web-Regular.ttf + done + done + - name: Rustfmt run: cargo fmt --all -- --check diff --git a/harfrust_capi/README.md b/harfrust_capi/README.md index 3a3e3868..4bbb607a 100644 --- a/harfrust_capi/README.md +++ b/harfrust_capi/README.md @@ -256,3 +256,15 @@ Enable the optional `subset` feature for Skera-backed subsetting in the same C library, using the existing face, blob, and set handles. Include `hr-subset.h` for the native API or `hr-hb-subset.h` for HarfBuzz spellings. See [SUBSETTING.md](SUBSETTING.md) for building, supported flags, and validation. + +## HarfBuzz C++ helpers + +Include `hr-hb-cplusplus.hh` in place of HarfBuzz's `hb-cplusplus.hh` for +the C++11 `hb::shared_ptr` and `hb::unique_ptr` ownership helpers, +their traits, and hash specializations. They adopt an existing reference; +copying a shared pointer acquires another reference. Supported core types +are blobs, buffers, faces, fonts, font functions, maps, sets, and shape plans. + +Including `hr-hb-subset.h` before or after the C++ header also enables +subset inputs and plans. Build `harfrust_capi` with `--features subset` +when using those objects. Objects without a complete HarfRust lifetime API have no vtable. diff --git a/harfrust_capi/include/hr-hb-cplusplus.hh b/harfrust_capi/include/hr-hb-cplusplus.hh new file mode 100644 index 00000000..e9bd27ec --- /dev/null +++ b/harfrust_capi/include/hr-hb-cplusplus.hh @@ -0,0 +1,227 @@ +/* + * Copyright © 2022 Behdad Esfahbod + * + * This is part of HarfBuzz, a text shaping library. + * + * Permission is hereby granted, without written agreement and without + * license or royalty fees, to use, copy, modify, and distribute this + * software and its documentation for any purpose, provided that the + * above copyright notice and the following two paragraphs appear in + * all copies of this software. + * + * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES + * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN + * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + * + * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS + * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + */ + +#ifndef HARFRUST_HB_CPLUSPLUS_HH +#define HARFRUST_HB_CPLUSPLUS_HH + +/* Adapted from HarfBuzz's hb-cplusplus.hh. Ownership helpers retain the + * hb namespace; user-data adapters accommodate HarfRust's const signatures. + * Only objects with the required HarfRust lifetime API are specialized. + */ +#include "hr-hb.h" + +#ifdef __cplusplus + +#include +#include +#include + +#if 0 +#if !(__cplusplus >= 201103L) +#error "HarfBuzz C++ helpers require C++11" +#endif +#endif + +namespace hb { + + +template +struct vtable; + +template +struct shared_ptr +{ + using element_type = T; + + using v = vtable; + + explicit shared_ptr (T *p = nullptr) : p (p) {} + shared_ptr (const shared_ptr &o) : p (v::reference (o.p)) {} + shared_ptr (shared_ptr &&o) noexcept : p (o.p) { o.p = nullptr; } + shared_ptr& operator = (const shared_ptr &o) { if (p != o.p) { destroy (); p = o.p; reference (); } return *this; } + shared_ptr& operator = (shared_ptr &&o) noexcept { v::destroy (p); p = o.p; o.p = nullptr; return *this; } + ~shared_ptr () { v::destroy (p); p = nullptr; } + + T* get() const { return p; } + + void swap (shared_ptr &o) noexcept { std::swap (p, o.p); } + friend void swap (shared_ptr &a, shared_ptr &b) noexcept { std::swap (a.p, b.p); } + + operator T * () const { return p; } + T& operator * () const { return *get (); } + T* operator -> () const { return get (); } + operator bool () const { return p; } + bool operator == (const shared_ptr &o) const { return p == o.p; } + bool operator != (const shared_ptr &o) const { return p != o.p; } + + static T* get_empty() { return v::get_empty (); } + T* reference() { return v::reference (p); } + void destroy() { v::destroy (p); } + void set_user_data (hb_user_data_key_t *key, + void *value, + hb_destroy_func_t destroy, + hb_bool_t replace) { v::set_user_data (p, key, value, destroy, replace); } + void * get_user_data (hb_user_data_key_t *key) { return v::get_user_data (p, key); } + + private: + T *p; +}; + +template struct is_shared_ptr : std::false_type {}; +template struct is_shared_ptr> : std::true_type {}; + +template +struct unique_ptr +{ + using element_type = T; + + using v = vtable; + + explicit unique_ptr (T *p = nullptr) : p (p) {} + unique_ptr (const unique_ptr &o) = delete; + unique_ptr (unique_ptr &&o) noexcept : p (o.p) { o.p = nullptr; } + unique_ptr& operator = (const unique_ptr &o) = delete; + unique_ptr& operator = (unique_ptr &&o) noexcept { v::destroy (p); p = o.p; o.p = nullptr; return *this; } + ~unique_ptr () { v::destroy (p); p = nullptr; } + + T* get() const { return p; } + T* release () { T* v = p; p = nullptr; return v; } + + void swap (unique_ptr &o) noexcept { std::swap (p, o.p); } + friend void swap (unique_ptr &a, unique_ptr &b) noexcept { std::swap (a.p, b.p); } + + operator T * () const { return p; } + T& operator * () const { return *get (); } + T* operator -> () const { return get (); } + operator bool () { return p; } + + private: + T *p; +}; + +template struct is_unique_ptr : std::false_type {}; +template struct is_unique_ptr> : std::true_type {}; + +template +struct vtable_t +{ + static constexpr auto get_empty = _get_empty; + static constexpr auto reference = _reference; + static constexpr auto destroy = _destroy; + static constexpr auto set_user_data = _set_user_data; + static constexpr auto get_user_data = _get_user_data; +}; + +/* HarfRust's key parameters are const, while several core user-data getters + * take a mutable object. Preserve the HarfBuzz vtable_t signature using these + * forwarding functions; they never modify the key or object themselves. */ +template +struct vtable_user_data; + +#define HB_DEFINE_VTABLE(name, empty) \ + template<> \ + struct vtable_user_data \ + { \ + static hb_bool_t set (hb_##name##_t *p, hb_user_data_key_t *key, \ + void *value, hb_destroy_func_t destroy, \ + hb_bool_t replace) \ + { return hb_##name##_set_user_data (p, key, value, destroy, replace); } \ + static void * get (const hb_##name##_t *p, hb_user_data_key_t *key) \ + { return hb_##name##_get_user_data (const_cast (p), key); } \ + }; \ + template<> \ + struct vtable \ + : vtable_t::set, \ + &vtable_user_data::get> {} + +HB_DEFINE_VTABLE (buffer, &hb_buffer_get_empty); +HB_DEFINE_VTABLE (blob, &hb_blob_get_empty); +HB_DEFINE_VTABLE (face, &hb_face_get_empty); +HB_DEFINE_VTABLE (font, &hb_font_get_empty); +HB_DEFINE_VTABLE (font_funcs, &hb_font_funcs_get_empty); +HB_DEFINE_VTABLE (map, &hb_map_get_empty); +HB_DEFINE_VTABLE (set, &hb_set_get_empty); +HB_DEFINE_VTABLE (shape_plan, &hb_shape_plan_get_empty); + + +#ifdef HARFRUST_HB_SUBSET_H + +HB_DEFINE_VTABLE (subset_input, nullptr); +HB_DEFINE_VTABLE (subset_plan, nullptr); + +#endif + + +/* Left available for the subset header's paired block when it is included + * after this header. */ + + +} // namespace hb + +/* Workaround for GCC < 7, see: + * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56480 + * https://stackoverflow.com/a/25594741 */ +namespace std { + + +template +struct hash> +{ + std::size_t operator()(const hb::shared_ptr& v) const noexcept + { + std::size_t h = std::hash{}(v.get ()); + return h; + } +}; + +template +struct hash> +{ + std::size_t operator()(const hb::unique_ptr& v) const noexcept + { + std::size_t h = std::hash{}(v.get ()); + return h; + } +}; + + +} // namespace std + +#endif /* __cplusplus */ + +#endif /* HARFRUST_HB_CPLUSPLUS_HH */ diff --git a/harfrust_capi/include/hr-hb-subset.h b/harfrust_capi/include/hr-hb-subset.h index 9d146b91..daa6fb23 100644 --- a/harfrust_capi/include/hr-hb-subset.h +++ b/harfrust_capi/include/hr-hb-subset.h @@ -60,4 +60,11 @@ #define HB_SUBSET_SETS_NO_SUBSET_TABLE_TAG HR_SUBSET_SETS_NO_SUBSET_TABLE_TAG #define HB_SUBSET_SETS_UNICODE HR_SUBSET_SETS_UNICODE +#if defined(__cplusplus) && defined(HARFRUST_HB_CPLUSPLUS_HH) +namespace hb { +HB_DEFINE_VTABLE (subset_input, nullptr); +HB_DEFINE_VTABLE (subset_plan, nullptr); +} // namespace hb +#endif + #endif /* HARFRUST_HB_SUBSET_H */ diff --git a/harfrust_capi/tests/cplusplus.cc b/harfrust_capi/tests/cplusplus.cc new file mode 100644 index 00000000..51fb5c05 --- /dev/null +++ b/harfrust_capi/tests/cplusplus.cc @@ -0,0 +1,129 @@ +// Exercise the HarfBuzz C++ ownership helpers against the actual C ABI. +#ifdef HR_TEST_SUBSET_FIRST +#include "hr-hb-subset.h" +#endif +#include "hr-hb-cplusplus.hh" +#ifdef HR_TEST_SUBSET_LAST +#include "hr-hb-subset.h" +#endif +// Repeated includes must not redefine the subset vtables. +#include "hr-hb-cplusplus.hh" +#if defined(HR_TEST_SUBSET_FIRST) || defined(HR_TEST_SUBSET_LAST) +#include "hr-hb-subset.h" +#endif + +#include +#include + +static hb_user_data_key_t key; +static unsigned destroyed; + +static void count_destroy(void *data) +{ + assert(data == &destroyed); + ++destroyed; +} + +template +static void check_ownership(T *object) +{ + const unsigned before = destroyed; + assert(object); + { + hb::shared_ptr owner(object); + owner.set_user_data(&key, &destroyed, count_destroy, true); + assert(owner.get_user_data(&key) == &destroyed); + { + hb::shared_ptr copy(owner); + hb::shared_ptr assigned; + assigned = copy; + hb::shared_ptr moved(std::move(copy)); + assert(!copy); + assert(moved == owner); + copy = std::move(assigned); + assert(!assigned); + assert(copy == owner); + std::unordered_set> set; + set.insert(owner); + set.insert(moved); + assert(set.size() == 1); + assert(destroyed == before); + } + assert(destroyed == before); + hb::unique_ptr unique(owner.reference()); + hb::unique_ptr moved(std::move(unique)); + assert(!unique); + unique = std::move(moved); + assert(!moved); + hb::unique_ptr released(unique.release()); + assert(!unique); + hb::unique_ptr empty; + swap(released, empty); + assert(!released); + assert(empty.get() == owner.get()); + assert(std::hash>{}(empty) == + std::hash{}(empty.get())); + } + assert(destroyed == before + 1); +} + +template +static void check_empty() +{ + hb::shared_ptr empty(hb::shared_ptr::get_empty()); + assert(empty); + hb::shared_ptr copy(empty); + assert(copy == empty); +} + +int main(int argc, char **argv) +{ + static_assert(hb::is_shared_ptr>::value, "shared trait"); + static_assert(hb::is_unique_ptr>::value, "unique trait"); + static_assert(!hb::is_shared_ptr::value, "raw pointer trait"); + static_assert(!std::is_copy_constructible>::value, + "unique ownership"); + static_assert(std::is_nothrow_move_constructible>::value, + "noexcept move"); + + check_empty(); + check_empty(); + check_empty(); + check_empty(); + check_empty(); + check_empty(); + check_empty(); + check_empty(); + + check_ownership(hb_blob_create("abc", 3, HB_MEMORY_MODE_DUPLICATE, nullptr, nullptr)); + check_ownership(hb_buffer_create()); + check_ownership(hb_font_funcs_create()); + check_ownership(hb_map_create()); + check_ownership(hb_set_create()); + + assert(argc == 2); + hb::shared_ptr blob(hb_blob_create_from_file_or_fail(argv[1])); + assert(blob && hb_blob_get_length(blob)); + hb::shared_ptr face(hb_face_create(blob, 0)); + assert(hb_face_get_glyph_count(face)); + check_ownership(hb_face_create(blob, 0)); + check_ownership(hb_font_create(face)); + + hb_segment_properties_t props = HB_SEGMENT_PROPERTIES_DEFAULT; + props.direction = HB_DIRECTION_LTR; + props.script = HB_SCRIPT_LATIN; + check_ownership(hb_shape_plan_create(face, &props, nullptr, 0, nullptr)); + +#if defined(HR_TEST_SUBSET_FIRST) || defined(HR_TEST_SUBSET_LAST) + check_ownership(hb_subset_input_create_or_fail()); + hb::shared_ptr input(hb_subset_input_create_or_fail()); + hb_set_add(hb_subset_input_unicode_set(input), 'A'); + check_ownership(hb_subset_plan_create_or_fail(face, input)); + hb::unique_ptr plan(hb_subset_plan_create_or_fail(face, input)); + assert(plan); + hb::shared_ptr result(hb_subset_plan_execute_or_fail(plan)); + assert(result && hb_face_get_glyph_count(result)); + hb::unique_ptr subset_blob(hb_face_reference_blob(result)); + assert(subset_blob && hb_blob_get_length(subset_blob)); +#endif +} diff --git a/scripts/gen-hb-compat-header.py b/scripts/gen-hb-compat-header.py index 07c8af9a..f4fcda0d 100644 --- a/scripts/gen-hb-compat-header.py +++ b/scripts/gen-hb-compat-header.py @@ -172,7 +172,12 @@ def main(): subset_lower, subset_upper = names(open(SUBSET_SOURCE, encoding="utf-8").read()) with open(SUBSET_TARGET, "w", encoding="utf-8", newline="\n") as f: f.write(render(subset_lower, subset_upper, subset_header)) - f.write("\n#endif /* HARFRUST_HB_SUBSET_H */\n") + f.write("\n#if defined(__cplusplus) && defined(HARFRUST_HB_CPLUSPLUS_HH)\n" + "namespace hb {\n" + "HB_DEFINE_VTABLE (subset_input, nullptr);\n" + "HB_DEFINE_VTABLE (subset_plan, nullptr);\n" + "} // namespace hb\n" + "#endif\n\n#endif /* HARFRUST_HB_SUBSET_H */\n") print("wrote %s (%d names)" % (TARGET, len(lower) + len(upper)))