From 3d88bcc051f8d02537d6fcc0aa89e1f9d4d0236a Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Mon, 27 Apr 2026 14:18:09 +0900 Subject: [PATCH] Add Cursor#copyable? and Type#copyable? Cursor#copyable? returns true when a class/struct cursor has an accessible copy constructor (none deleted or private) and every base class is copyable. Returns true for non-class cursors so callers can ask the question uniformly without dispatching on kind. Type#copyable? wraps the cursor predicate, stripping references first so the question is asked of the underlying type rather than the reference itself. Pairs with the existing Cursor#copy_constructor?, #deleted?, and #private? predicates already in ffi-clang. Generic C++ introspection useful for binding-generation tools deciding whether a type is safe to pass by value. --- lib/ffi/clang/cursor.rb | 27 +++++++++++++++++ lib/ffi/clang/types/type.rb | 11 +++++++ spec/ffi/clang/cursor_spec.rb | 49 +++++++++++++++++++++++++++++++ spec/ffi/clang/fixtures/types.cxx | 13 ++++++++ spec/ffi/clang/type_spec.rb | 35 ++++++++++++++++++++++ 5 files changed, 135 insertions(+) diff --git a/lib/ffi/clang/cursor.rb b/lib/ffi/clang/cursor.rb index 14e14e8..6b89f68 100644 --- a/lib/ffi/clang/cursor.rb +++ b/lib/ffi/clang/cursor.rb @@ -813,6 +813,33 @@ def abstract? Lib.is_abstract(@cursor) != 0 end + # Check if this class/struct is copyable: it has no inaccessible + # (deleted or private) copy constructor, and every base class is + # copyable (recursively). Returns true on cursors that don't denote + # a class/struct, since the question doesn't apply. + # + # A class with no explicit copy constructor is treated as copyable — + # the implicit one is generated. We only flag explicit `= delete` or + # private/protected access. + # + # @returns [Boolean] True if instances of this type can be copied. + def copyable? + return true unless [:cursor_class_decl, :cursor_struct].include?(self.kind) + + copy_constructors = self.find_by_kind(false, :cursor_constructor).select(&:copy_constructor?) + copy_constructors.each do |constructor| + return false if constructor.deleted? || constructor.private? + end + + self.find_by_kind(false, :cursor_cxx_base_specifier).each do |base| + base_decl = base.type.declaration + next if base_decl.kind == :cursor_no_decl_found + return false unless base_decl.copyable? + end + + true + end + # Check if this is a scoped enum. # @returns [Boolean] True if it's a scoped enum. def enum_scoped? diff --git a/lib/ffi/clang/types/type.rb b/lib/ffi/clang/types/type.rb index 1ca3288..5a8ecf5 100644 --- a/lib/ffi/clang/types/type.rb +++ b/lib/ffi/clang/types/type.rb @@ -172,6 +172,17 @@ def intrinsic_type type.unqualified_type end + # Check if this type's declaration (after reference stripping) + # has an accessible copy constructor and copyable bases. + # Returns true for non-class types (fundamentals, pointers, + # enums) and for types whose declaration is unavailable + # (:cursor_no_decl_found). + # + # @returns [Boolean] True if instances of this type can be copied. + def copyable? + self.non_reference_type.declaration.copyable? + end + # Get the type of a template argument at the given index. # For template specializations (e.g., `std::vector`), this returns the type of # the template argument at the specified position. diff --git a/spec/ffi/clang/cursor_spec.rb b/spec/ffi/clang/cursor_spec.rb index a9bd739..593cfb0 100644 --- a/spec/ffi/clang/cursor_spec.rb +++ b/spec/ffi/clang/cursor_spec.rb @@ -1463,6 +1463,55 @@ end end + describe "#copyable?" do + let(:simple_struct) do + find_matching(cursor_types) do |child, parent| + child.kind == :cursor_struct and child.spelling == "SimpleStruct" + end + end + + let(:deleted_copy) do + find_matching(cursor_types) do |child, parent| + child.kind == :cursor_struct and child.spelling == "DeletedCopy" + end + end + + let(:private_copy) do + find_matching(cursor_types) do |child, parent| + child.kind == :cursor_class_decl and child.spelling == "PrivateCopy" + end + end + + let(:inherits_deleted_copy) do + find_matching(cursor_types) do |child, parent| + child.kind == :cursor_struct and child.spelling == "InheritsDeletedCopy" + end + end + + it "returns true for a struct with implicit copy constructor" do + expect(simple_struct.copyable?).to be true + end + + it "returns false for a struct with deleted copy constructor" do + expect(deleted_copy.copyable?).to be false + end + + it "returns false for a class with private copy constructor" do + expect(private_copy.copyable?).to be false + end + + it "returns false transitively when a base class is not copyable" do + expect(inherits_deleted_copy.copyable?).to be false + end + + it "returns true on a non-class cursor" do + func = find_matching(cursor_types) do |child, parent| + child.kind == :cursor_function and child.spelling == "binary_operator_func" + end + expect(func.copyable?).to be true + end + end + describe "#copy_assignment_operator?" do let(:copy_assign) do find_matching(cursor_types) do |child, parent| diff --git a/spec/ffi/clang/fixtures/types.cxx b/spec/ffi/clang/fixtures/types.cxx index 59bdd68..db14d2f 100644 --- a/spec/ffi/clang/fixtures/types.cxx +++ b/spec/ffi/clang/fixtures/types.cxx @@ -49,6 +49,19 @@ struct DeletedCopy { DeletedCopy& operator=(DeletedCopy&&) = default; }; +// Private copy constructor — not copyable. +class PrivateCopy { +private: + PrivateCopy(const PrivateCopy&); +public: + PrivateCopy(); +}; + +// Inherits from a non-copyable base — not transitively copyable. +struct InheritsDeletedCopy : public DeletedCopy { + int value; +}; + // Explicit constructor struct ExplicitCtor { explicit ExplicitCtor(int x); diff --git a/spec/ffi/clang/type_spec.rb b/spec/ffi/clang/type_spec.rb index db14be2..8838a2a 100644 --- a/spec/ffi/clang/type_spec.rb +++ b/spec/ffi/clang/type_spec.rb @@ -282,6 +282,41 @@ end end + describe "#copyable?" do + let(:cursor_types) {Index.new.parse_translation_unit(fixture_path("types.cxx")).cursor} + + it "returns true for a copyable struct type" do + type = find_matching(cursor_types) do |child, parent| + child.kind == :cursor_struct and child.spelling == "SimpleStruct" + end.type + expect(type.copyable?).to be true + end + + it "returns false for a struct type with deleted copy constructor" do + type = find_matching(cursor_types) do |child, parent| + child.kind == :cursor_struct and child.spelling == "DeletedCopy" + end.type + expect(type.copyable?).to be false + end + + it "strips a reference before consulting the declaration" do + # A reference to DeletedCopy should be reported as not copyable — + # copyable? operates on the underlying type, not the reference. + ref_type = find_matching(cursor_cxx) do |child, parent| + child.kind == :cursor_cxx_method and child.spelling == "takesARef" + end.type.arg_types.to_a[0] + expect(ref_type.kind).to eq(:type_lvalue_ref) + expect(ref_type.copyable?).to be true # int& → int → copyable + end + + it "returns true for a fundamental type" do + int_type = find_matching(cursor_cxx) do |child, parent| + child.kind == :cursor_field_decl and child.spelling == "int_member_a" + end.type + expect(int_type.copyable?).to be true + end + end + describe "#const_qualified?" do let(:pointer_type) do find_matching(cursor_cxx) do |child, parent|