Skip to content

Commit 631af50

Browse files
committed
Revert overly broad Rice filtering heuristics
1 parent 7d263c8 commit 631af50

File tree

9 files changed

+162
-66
lines changed

9 files changed

+162
-66
lines changed

lib/ruby-bindgen/generators/rice/rice.rb

Lines changed: 46 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ def initialize(inputter, outputter, config)
180180
@incomplete_iterators = Hash.new
181181
# Iterator names per class (for aliasing each_const -> each)
182182
@class_iterator_names = Hash.new { |h, k| h[k] = Set.new }
183+
@project_complete_types = Set.new
183184
end
184185

185186
# Parse the configured inputs with libclang and stream the resulting
@@ -188,6 +189,7 @@ def generate
188189
clang_args = @config[:clang_args] || []
189190
parser = RubyBindgen::Parser.new(@inputter, clang_args, libclang: @config[:libclang])
190191
::FFI::Clang::Cursor.namer = @namer
192+
build_project_complete_type_index(parser)
191193
parser.generate(self)
192194
end
193195

@@ -291,6 +293,7 @@ def unsupported_rice_opaque_namespace_type?(type)
291293
decl = type.canonical.declaration
292294
return false if decl.kind == :cursor_no_decl_found
293295
return false unless decl.opaque_declaration?
296+
return false if project_complete_type?(decl)
294297
return false if decl.qualified_name&.start_with?("std::", "__gnu_cxx::")
295298
return false if [:cursor_class_decl, :cursor_struct].include?(decl.semantic_parent.kind)
296299

@@ -449,7 +452,6 @@ def visit_translation_unit(translation_unit, path, relative_path)
449452
@non_member_operators.clear
450453
@incomplete_iterators.clear
451454
@class_iterator_names.clear
452-
@declared_function_qualified_names = nil
453455
cursor = translation_unit.cursor
454456
@translation_unit_cursor = cursor
455457
@type_speller.printing_policy = cursor.printing_policy
@@ -516,6 +518,40 @@ def visit_translation_unit(translation_unit, path, relative_path)
516518
self.outputter.write(rice_header, content)
517519
end
518520

521+
def build_project_complete_type_index(parser)
522+
@project_complete_types.clear
523+
524+
@inputter.each do |path, _relative_path|
525+
begin
526+
translation_unit = parser.send(:parse_translation_unit, path)
527+
rescue RubyBindgen::Parser::ParseError
528+
next
529+
end
530+
531+
record_project_complete_types(translation_unit.cursor)
532+
end
533+
end
534+
535+
def record_project_complete_types(cursor)
536+
cursor.find_by_kind(true, :cursor_class_decl, :cursor_struct) do |child|
537+
next if child.spelling.empty?
538+
next if child.opaque_declaration?
539+
next unless translation_unit_file?(child)
540+
541+
qualified_name = child.qualified_name
542+
next if qualified_name.nil? || qualified_name.empty?
543+
544+
@project_complete_types << qualified_name
545+
end
546+
end
547+
548+
def project_complete_type?(decl)
549+
qualified_name = decl.qualified_name
550+
return false if qualified_name.nil? || qualified_name.empty?
551+
552+
@project_complete_types.include?(qualified_name)
553+
end
554+
519555
# Render a public, callable constructor into the Rice chain for its class.
520556
def visit_constructor(cursor)
521557
# Do not process class constructors defined outside of the class definition
@@ -553,16 +589,7 @@ def visit_constructor(cursor)
553589
# constants, embedded types, and any auto-generated template bases needed
554590
# before the class itself can be registered.
555591
def visit_class_decl(cursor)
556-
# Namespace-scope forward-declared C++ classes are often completed in a
557-
# different header. Emitting a Rice class here creates a Ruby constant
558-
# with no superclass, and a later complete definition then conflicts.
559-
# Keep nested incomplete classes on the existing special path, and keep
560-
# opaque structs available for handle-style APIs.
561-
if cursor.kind == :cursor_class_decl &&
562-
cursor.opaque_declaration? &&
563-
![:cursor_class_decl, :cursor_struct].include?(cursor.semantic_parent.kind)
564-
return
565-
end
592+
return if skip_namespace_forward_declaration?(cursor)
566593

567594
# Skip explicitly listed symbols
568595
return if skip_symbol?(cursor)
@@ -896,28 +923,12 @@ def skip_callable?(cursor)
896923
cursor.type.variadic?
897924
end
898925

899-
def unresolved_inline_calls?(cursor)
900-
source = inline_definition_source(cursor)
901-
return false unless source&.include?('{')
902-
903-
function_calls = source.scan(/([A-Za-z_]\w*(?:::[A-Za-z_]\w*)+)\s*\(/)
904-
.flatten
905-
.uniq
906-
.select { |name| name.split('::').last.match?(/\A[a-z_]\w*\z/) }
907-
return false if function_calls.empty?
908-
909-
function_calls.any? do |qualified_name|
910-
!declared_function_qualified_names.include?(qualified_name)
911-
end
912-
end
913-
914926
# Render a class method, including special handling for iterator adapters
915927
# and mutable `operator[]` setter support.
916928
def visit_cxx_method(cursor)
917929
# Do not process method definitions outside of classes (because we already processed them)
918930
return if cursor.lexical_parent != cursor.semantic_parent
919931
return if skip_callable?(cursor)
920-
return if unresolved_inline_calls?(cursor)
921932
return if has_skipped_param_type?(cursor)
922933
return if has_unsupported_rice_param_type?(cursor)
923934
return if has_skipped_return_type?(cursor)
@@ -965,43 +976,16 @@ def visit_cxx_method(cursor)
965976
result
966977
end
967978

968-
def declared_function_qualified_names
969-
@declared_function_qualified_names ||= @translation_unit_cursor
970-
.find_by_kind(true, :cursor_function, :cursor_function_template)
971-
.map(&:qualified_name)
972-
.reject(&:empty?)
973-
.to_set
974-
end
979+
def skip_namespace_forward_declaration?(cursor)
980+
return false unless cursor.kind == :cursor_class_decl
981+
return false unless cursor.opaque_declaration?
982+
return false if [:cursor_class_decl, :cursor_struct].include?(cursor.semantic_parent.kind)
975983

976-
def inline_definition_source(cursor)
977-
parent = cursor.semantic_parent
978-
source = parent&.extent&.text
979-
return nil if source.nil? || source.empty?
980-
981-
line_index = cursor.extent.start.line - parent.extent.start.line
982-
lines = source.lines
983-
return nil if line_index.negative? || line_index >= lines.length
984-
985-
result = String.new
986-
brace_depth = 0
987-
saw_body = false
988-
989-
lines[line_index..].each do |line|
990-
result << line
991-
line.each_char do |char|
992-
if char == '{'
993-
saw_body = true
994-
brace_depth += 1
995-
elsif char == '}'
996-
brace_depth -= 1 if brace_depth.positive?
997-
end
998-
end
999-
1000-
break if saw_body && brace_depth.zero?
1001-
break if !saw_body && line.include?(';')
1002-
end
984+
definition = cursor.definition
985+
return false if [:cursor_invalid_file, :cursor_no_decl_found].include?(definition.kind)
986+
return false if definition.opaque_declaration?
1003987

1004-
result
988+
translation_unit_file?(definition)
1005989
end
1006990

1007991
# Check if an iterator type has proper std::iterator_traits.

test/bindings/cpp/unresolved_inline_calls-rb.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,12 @@ void Init_UnresolvedInlineCalls()
1212
Module rb_mTests = define_module("Tests");
1313

1414
Module rb_mTestsBroken = define_module_under(rb_mTests, "Broken");
15+
16+
Rice::Data_Type<Tests::Logger> rb_cTestsLogger = define_class_under<Tests::Logger>(rb_mTests, "Logger")
17+
.define_constructor(Constructor<Tests::Logger>())
18+
.define_singleton_function<int(*)()>("info", &Tests::Logger::info);
19+
20+
Module rb_mTestsUtils = define_module_under(rb_mTests, "Utils");
21+
22+
rb_mTestsUtils.define_module_function<int(*)()>("id", &Tests::Utils::id);
1523
}

test/bindings/cpp/unresolved_inline_calls-rb.ipp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ template<typename T>
44
inline Rice::Data_Type<Tests::Params<T>> Params_instantiate(Rice::Module parent, const char* name)
55
{
66
return Rice::define_class_under<Tests::Params<T>>(parent, name)
7+
.template define_method<int(Tests::Params<T>::*)() const>("backend", &Tests::Params<T>::backend)
8+
.template define_method<int(Tests::Params<T>::*)() const>("logger", &Tests::Params<T>::logger)
9+
.template define_method<int(Tests::Params<T>::*)() const>("utility", &Tests::Params<T>::utility)
710
.template define_method<int(Tests::Params<T>::*)() const>("ok", &Tests::Params<T>::ok);
811
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#include "opaque_namespace_handle.hpp"
2+
3+
namespace Tests
4+
{
5+
class OpaqueNamespaceConsumer
6+
{
7+
public:
8+
OpaqueNamespaceConsumer();
9+
OpaqueNamespaceConsumer(const Render::Handle& handle);
10+
11+
const Render::Handle& inHandle() const;
12+
Render::Handle outHandle() const;
13+
Render::Handle& outHandleRef();
14+
void setHandle(const Render::Handle& handle);
15+
16+
int value() const;
17+
};
18+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#include "opaque_namespace_handle.hpp"
2+
3+
namespace Tests
4+
{
5+
namespace Render
6+
{
7+
class Handle
8+
{
9+
public:
10+
int id;
11+
};
12+
}
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Tests
2+
{
3+
namespace Render
4+
{
5+
class Handle;
6+
}
7+
}

test/headers/cpp/unresolved_inline_calls.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,22 @@ namespace Tests {
44
namespace Broken {
55
}
66

7+
class Logger
8+
{
9+
public:
10+
static int info();
11+
};
12+
13+
namespace Utils {
14+
int id();
15+
}
16+
717
template<typename T>
818
struct Params
919
{
1020
int backend() const { return Tests::Broken::backend(); }
21+
int logger() const { return Tests::Logger::info(); }
22+
int utility() const { return Utils::id(); }
1123
int ok() const { return 1; }
1224
};
1325
}

test/rice_generator_test.rb

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ class FunctionTemplate
176176
assert_includes rendered, "FunctionTemplate_instantiate<&Tests::callback_ints>"
177177
end
178178

179-
def test_namespace_scope_forward_declared_classes_do_not_generate_bindings
179+
def test_namespace_scope_forward_declared_classes_generate_placeholders_across_headers
180180
config_dir = File.join(__dir__, "headers", "cpp")
181181
config = load_config(config_dir)
182182
inputter = RubyBindgen::Inputter.new(config_dir, ["forward_declared_classes.hpp",
@@ -191,12 +191,61 @@ def test_namespace_scope_forward_declared_classes_do_not_generate_bindings
191191
classes_cpp = outputter.output_paths.fetch(outputter.output_path("forward_declared_classes-rb.cpp"))
192192
all_layers_cpp = outputter.output_paths.fetch(outputter.output_path("forward_declared_all_layers-rb.cpp"))
193193

194-
refute_includes classes_cpp,
194+
assert_includes classes_cpp,
195195
'define_class_under<ForwardDeclaredClasses::ActivationLayer>(rb_mForwardDeclaredClasses, "ActivationLayer")'
196196
assert_includes all_layers_cpp,
197197
'define_class_under<ForwardDeclaredClasses::ActivationLayer, ForwardDeclaredClasses::Layer>(rb_mForwardDeclaredClasses, "ActivationLayer")'
198198
end
199199

200+
def test_same_file_forward_declarations_only_render_the_complete_class
201+
parsed, = parse_cpp(<<~CPP)
202+
namespace Tests {
203+
class Forward;
204+
205+
class Base {
206+
public:
207+
virtual ~Base() = default;
208+
};
209+
210+
class Forward : public Base {
211+
public:
212+
int value() const { return 1; }
213+
};
214+
}
215+
CPP
216+
217+
inputter = RubyBindgen::Inputter.new(parsed.dir, ["fixture.hpp"])
218+
outputter = create_outputter("cpp")
219+
config = load_config(File.join(__dir__, "headers", "cpp"))
220+
generator = RubyBindgen::Generators::Rice.new(inputter, outputter, config)
221+
222+
capture_io { generator.generate }
223+
224+
generated_cpp = outputter.output_paths.fetch(outputter.output_path("fixture-rb.cpp"))
225+
226+
assert_equal 1, generated_cpp.scan('define_class_under<Tests::Forward, Tests::Base>(rb_mTests, "Forward")').size
227+
refute_includes generated_cpp, 'define_class_under<Tests::Forward>(rb_mTests, "Forward")'
228+
end
229+
230+
def test_namespace_scope_opaque_types_with_project_definitions_are_generated
231+
config_dir = File.join(__dir__, "headers", "cpp")
232+
config = load_config(config_dir)
233+
inputter = RubyBindgen::Inputter.new(config_dir, ["opaque_namespace_api.hpp",
234+
"opaque_namespace_full.hpp"])
235+
outputter = create_outputter("cpp")
236+
generator = RubyBindgen::Generators::Rice.new(inputter, outputter, config)
237+
238+
capture_io { generator.generate }
239+
240+
generated_cpp = outputter.output_paths.fetch(outputter.output_path("opaque_namespace_api-rb.cpp"))
241+
242+
assert_includes generated_cpp, "Constructor<Tests::OpaqueNamespaceConsumer, const Tests::Render::Handle &>()"
243+
assert_includes generated_cpp, '"in_handle", &Tests::OpaqueNamespaceConsumer::inHandle'
244+
assert_includes generated_cpp, '"out_handle", &Tests::OpaqueNamespaceConsumer::outHandle'
245+
assert_includes generated_cpp, '"out_handle_ref", &Tests::OpaqueNamespaceConsumer::outHandleRef'
246+
assert_includes generated_cpp, '"set_handle", &Tests::OpaqueNamespaceConsumer::setHandle'
247+
end
248+
200249
def test_out_of_class_nested_definitions_are_rendered_only_under_the_parent
201250
parsed, = parse_cpp(<<~CPP)
202251
namespace Tests {

test/rice_test.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ def test_class_template_typedef_qualification
552552
end
553553
end
554554

555-
def test_unresolved_inline_calls_are_skipped
555+
def test_inline_methods_with_qualified_calls_are_generated
556556
config_dir = File.join(__dir__, "headers", "cpp")
557557
config = load_config(config_dir)
558558
config[:match] = ["unresolved_inline_calls.hpp"]
@@ -565,7 +565,9 @@ def test_unresolved_inline_calls_are_skipped
565565

566566
generated_ipp = outputter.output_paths.fetch(outputter.output_path("unresolved_inline_calls-rb.ipp"))
567567

568-
refute_includes generated_ipp, "\"backend\", &Tests::Params<T>::backend"
568+
assert_includes generated_ipp, "\"backend\", &Tests::Params<T>::backend"
569+
assert_includes generated_ipp, "\"logger\", &Tests::Params<T>::logger"
570+
assert_includes generated_ipp, "\"utility\", &Tests::Params<T>::utility"
569571
assert_includes generated_ipp, "\"ok\", &Tests::Params<T>::ok"
570572

571573
expected_cpp = outputter.output_path("unresolved_inline_calls-rb.cpp")

0 commit comments

Comments
 (0)