ruby-bindgen 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +25 -0
  4. data/README.md +68 -0
  5. data/Rakefile +8 -0
  6. data/bin/ruby-bindgen +133 -0
  7. data/docs/architecture.md +238 -0
  8. data/docs/c/c_bindings.md +65 -0
  9. data/docs/c/constants.md +21 -0
  10. data/docs/c/customizing.md +19 -0
  11. data/docs/c/filtering.md +24 -0
  12. data/docs/c/getting_started.md +56 -0
  13. data/docs/c/library_loading.md +53 -0
  14. data/docs/c/output.md +96 -0
  15. data/docs/c/types.md +61 -0
  16. data/docs/c/version_guards.md +105 -0
  17. data/docs/cmake/cmake_bindings.md +19 -0
  18. data/docs/cmake/filtering.md +26 -0
  19. data/docs/cmake/getting_started.md +52 -0
  20. data/docs/cmake/output.md +110 -0
  21. data/docs/configuration.md +351 -0
  22. data/docs/contributing.md +68 -0
  23. data/docs/cpp/buffers.md +24 -0
  24. data/docs/cpp/classes.md +139 -0
  25. data/docs/cpp/cpp_bindings.md +29 -0
  26. data/docs/cpp/customizing.md +124 -0
  27. data/docs/cpp/enums.md +42 -0
  28. data/docs/cpp/filtering.md +35 -0
  29. data/docs/cpp/getting_started.md +80 -0
  30. data/docs/cpp/iterators.md +94 -0
  31. data/docs/cpp/operators.md +170 -0
  32. data/docs/cpp/output.md +125 -0
  33. data/docs/cpp/templates.md +114 -0
  34. data/docs/examples.md +133 -0
  35. data/docs/index.md +133 -0
  36. data/docs/prior_art.md +37 -0
  37. data/docs/troubleshooting.md +243 -0
  38. data/docs/type_spelling.md +200 -0
  39. data/docs/updating_bindings.md +55 -0
  40. data/docs/version_guards.md +69 -0
  41. data/lib/ruby-bindgen/config.rb +63 -0
  42. data/lib/ruby-bindgen/generators/cmake/cmake.rb +171 -0
  43. data/lib/ruby-bindgen/generators/cmake/directory.erb +29 -0
  44. data/lib/ruby-bindgen/generators/cmake/guard.rb +55 -0
  45. data/lib/ruby-bindgen/generators/cmake/presets.erb +232 -0
  46. data/lib/ruby-bindgen/generators/cmake/project.erb +89 -0
  47. data/lib/ruby-bindgen/generators/ffi/callback.erb +1 -0
  48. data/lib/ruby-bindgen/generators/ffi/constant.erb +1 -0
  49. data/lib/ruby-bindgen/generators/ffi/enum_constant_decl.erb +1 -0
  50. data/lib/ruby-bindgen/generators/ffi/enum_decl.erb +4 -0
  51. data/lib/ruby-bindgen/generators/ffi/enum_decl_anonymous.erb +4 -0
  52. data/lib/ruby-bindgen/generators/ffi/ffi.rb +687 -0
  53. data/lib/ruby-bindgen/generators/ffi/field_decl.erb +1 -0
  54. data/lib/ruby-bindgen/generators/ffi/function.erb +5 -0
  55. data/lib/ruby-bindgen/generators/ffi/library.erb +39 -0
  56. data/lib/ruby-bindgen/generators/ffi/project.erb +18 -0
  57. data/lib/ruby-bindgen/generators/ffi/struct.erb +6 -0
  58. data/lib/ruby-bindgen/generators/ffi/translation_unit.erb +9 -0
  59. data/lib/ruby-bindgen/generators/ffi/typedef_decl.erb +1 -0
  60. data/lib/ruby-bindgen/generators/ffi/union.erb +6 -0
  61. data/lib/ruby-bindgen/generators/ffi/variable.erb +1 -0
  62. data/lib/ruby-bindgen/generators/ffi/version.erb +9 -0
  63. data/lib/ruby-bindgen/generators/ffi/version_method.erb +5 -0
  64. data/lib/ruby-bindgen/generators/generator.rb +52 -0
  65. data/lib/ruby-bindgen/generators/rice/auto_generated_base_class.erb +5 -0
  66. data/lib/ruby-bindgen/generators/rice/class.erb +31 -0
  67. data/lib/ruby-bindgen/generators/rice/class_template.erb +9 -0
  68. data/lib/ruby-bindgen/generators/rice/class_template_specialization.erb +10 -0
  69. data/lib/ruby-bindgen/generators/rice/constant.erb +9 -0
  70. data/lib/ruby-bindgen/generators/rice/constructor.erb +1 -0
  71. data/lib/ruby-bindgen/generators/rice/conversion_function.erb +4 -0
  72. data/lib/ruby-bindgen/generators/rice/cxx_iterator_method.erb +1 -0
  73. data/lib/ruby-bindgen/generators/rice/cxx_method.erb +6 -0
  74. data/lib/ruby-bindgen/generators/rice/enum_constant_decl.erb +7 -0
  75. data/lib/ruby-bindgen/generators/rice/enum_decl.erb +6 -0
  76. data/lib/ruby-bindgen/generators/rice/field_decl.erb +8 -0
  77. data/lib/ruby-bindgen/generators/rice/function.erb +6 -0
  78. data/lib/ruby-bindgen/generators/rice/function_pointer.rb +68 -0
  79. data/lib/ruby-bindgen/generators/rice/incomplete_class.erb +3 -0
  80. data/lib/ruby-bindgen/generators/rice/iterator_alias.erb +1 -0
  81. data/lib/ruby-bindgen/generators/rice/iterator_collector.rb +159 -0
  82. data/lib/ruby-bindgen/generators/rice/namespace.erb +5 -0
  83. data/lib/ruby-bindgen/generators/rice/non_member_operator_binary.erb +4 -0
  84. data/lib/ruby-bindgen/generators/rice/non_member_operator_inspect.erb +6 -0
  85. data/lib/ruby-bindgen/generators/rice/non_member_operator_unary.erb +4 -0
  86. data/lib/ruby-bindgen/generators/rice/operator[].erb +4 -0
  87. data/lib/ruby-bindgen/generators/rice/project.cpp.erb +18 -0
  88. data/lib/ruby-bindgen/generators/rice/project.hpp.erb +13 -0
  89. data/lib/ruby-bindgen/generators/rice/reference_qualifier.rb +495 -0
  90. data/lib/ruby-bindgen/generators/rice/rice.rb +1724 -0
  91. data/lib/ruby-bindgen/generators/rice/rice_include.hpp.erb +7 -0
  92. data/lib/ruby-bindgen/generators/rice/signature_builder.rb +230 -0
  93. data/lib/ruby-bindgen/generators/rice/template_resolver.rb +585 -0
  94. data/lib/ruby-bindgen/generators/rice/translation_unit.cpp.erb +40 -0
  95. data/lib/ruby-bindgen/generators/rice/translation_unit.hpp.erb +7 -0
  96. data/lib/ruby-bindgen/generators/rice/translation_unit.ipp.erb +3 -0
  97. data/lib/ruby-bindgen/generators/rice/type_index.rb +117 -0
  98. data/lib/ruby-bindgen/generators/rice/type_speller.rb +509 -0
  99. data/lib/ruby-bindgen/generators/rice/union.erb +5 -0
  100. data/lib/ruby-bindgen/generators/rice/variable.erb +5 -0
  101. data/lib/ruby-bindgen/inputter.rb +54 -0
  102. data/lib/ruby-bindgen/name_mapper.rb +65 -0
  103. data/lib/ruby-bindgen/namer.rb +138 -0
  104. data/lib/ruby-bindgen/outputter.rb +40 -0
  105. data/lib/ruby-bindgen/parser.rb +82 -0
  106. data/lib/ruby-bindgen/refinements/cursor.rb +57 -0
  107. data/lib/ruby-bindgen/refinements/string.rb +41 -0
  108. data/lib/ruby-bindgen/symbol_candidates.rb +282 -0
  109. data/lib/ruby-bindgen/symbol_entry.rb +21 -0
  110. data/lib/ruby-bindgen/symbols.rb +107 -0
  111. data/lib/ruby-bindgen/type_pointer_formatter.rb +35 -0
  112. data/lib/ruby-bindgen/version.rb +3 -0
  113. data/lib/ruby-bindgen.rb +19 -0
  114. data/ruby-bindgen.gemspec +52 -0
  115. metadata +260 -0
@@ -0,0 +1,1724 @@
1
+ require 'set'
2
+
3
+ # Forward declaration so the helper files below can nest their classes
4
+ # under Rice (Ruby would otherwise see `class Rice` with no superclass
5
+ # in the helpers and TypeError when this file later says `< Generator`).
6
+ module RubyBindgen
7
+ module Generators
8
+ class Rice < Generator
9
+ end
10
+ end
11
+ end
12
+
13
+ require_relative 'function_pointer'
14
+ require_relative 'iterator_collector'
15
+ require_relative 'reference_qualifier'
16
+ require_relative 'signature_builder'
17
+ require_relative 'template_resolver'
18
+ require_relative 'type_index'
19
+ require_relative 'type_speller'
20
+
21
+ module RubyBindgen
22
+ module Generators
23
+ class Rice < Generator
24
+ CURSOR_LITERALS = [:cursor_integer_literal, :cursor_floating_literal,
25
+ :cursor_imaginary_literal, :cursor_string_literal,
26
+ :cursor_character_literal, :cursor_cxx_bool_literal_expr,
27
+ :cursor_cxx_null_ptr_literal_expr, :cursor_fixed_point_literal,
28
+ :cursor_unary_operator]
29
+
30
+ CURSOR_CLASSES = [:cursor_class_decl, :cursor_class_template, :cursor_struct]
31
+
32
+ # Fundamental types that should use ArgBuffer/ReturnBuffer when passed/returned as pointers
33
+ # Mapping of C++ operators to Ruby method names per Rice documentation
34
+ # Keys use cursor spelling form (e.g., 'operator()') so they share the same
35
+ # key namespace as user rename_methods.
36
+ # Values can be:
37
+ # - String: direct mapping
38
+ # - Proc: called with cursor to determine mapping (for arity-dependent operators)
39
+ OPERATOR_MAPPINGS = RubyBindgen::NameMapper.new([
40
+ # Assignment operators - not overridable in Ruby
41
+ ['operator=', 'assign'],
42
+ ['operator+=', 'assign_plus'],
43
+ ['operator-=', 'assign_minus'],
44
+ ['operator*=', 'assign_multiply'],
45
+ ['operator/=', 'assign_divide'],
46
+ ['operator%=', 'assign_modulus'],
47
+
48
+ # Bitwise assignment operators - not overridable in Ruby
49
+ ['operator&=', 'assign_and'],
50
+ ['operator|=', 'assign_or'],
51
+ ['operator^=', 'assign_xor'],
52
+ ['operator<<=', 'assign_left_shift'],
53
+ ['operator>>=', 'assign_right_shift'],
54
+
55
+ # Logical operators - && and || not overridable in Ruby
56
+ ['operator&&', 'logical_and'],
57
+ ['operator||', 'logical_or'],
58
+
59
+ # Function call operator
60
+ ['operator()', 'call'],
61
+
62
+ # Member access through pointer (arrow operator)
63
+ ['operator->', 'arrow'],
64
+
65
+ # Increment/decrement - arity-dependent (prefix=0 args, postfix=1 arg)
66
+ ['operator++', ->(cursor) { cursor.type.args_size == 0 ? 'increment' : 'increment_post' }],
67
+ ['operator--', ->(cursor) { cursor.type.args_size == 0 ? 'decrement' : 'decrement_post' }],
68
+
69
+ # Dereference vs multiply - arity-dependent (unary=0 args, binary=1 arg)
70
+ ['operator*', ->(cursor) { cursor.type.args_size == 0 ? 'dereference' : '*' }],
71
+
72
+ # Unary plus/minus vs binary - arity-dependent
73
+ # Ruby uses +@ and -@ for unary operators, + and - for binary
74
+ # Member: unary=0 args, binary=1 arg
75
+ # Non-member: unary=1 arg, binary=2 args (but we check member arity here)
76
+ ['operator+', ->(cursor) { cursor.type.args_size == 0 ? '+@' : '+' }],
77
+ ['operator-', ->(cursor) { cursor.type.args_size == 0 ? '-@' : '-' }],
78
+
79
+ # Pass-through operators - Ruby supports these directly
80
+ ['operator/', '/'],
81
+ ['operator%', '%'],
82
+ ['operator&', '&'],
83
+ ['operator|', '|'],
84
+ ['operator^', '^'],
85
+ ['operator~', '~'],
86
+ ['operator<<', '<<'],
87
+ ['operator>>', '>>'],
88
+ ['operator==', '=='],
89
+ ['operator!=', '!='],
90
+ ['operator<', '<'],
91
+ ['operator>', '>'],
92
+ ['operator<=', '<='],
93
+ ['operator>=', '>='],
94
+ ['operator!', '!'],
95
+ ['operator[]', '[]'],
96
+ ]).freeze
97
+
98
+ # Mapping of C++ type names to Ruby conversion method suffixes
99
+ CONVERSION_TYPE_MAPPINGS = RubyBindgen::NameMapper.new([
100
+ # Standard integer types
101
+ ['int', 'i'],
102
+ ['long', 'l'],
103
+ ['long long', 'i64'],
104
+ ['short', 'i16'],
105
+ ['unsigned int', 'u'],
106
+ ['unsigned long', 'ul'],
107
+ ['unsigned long long', 'u64'],
108
+ ['unsigned short', 'u16'],
109
+ # Fixed-width integer types
110
+ ['int8_t', 'i8'],
111
+ ['int16_t', 'i16'],
112
+ ['int32_t', 'i32'],
113
+ ['int64_t', 'i64'],
114
+ ['uint8_t', 'u8'],
115
+ ['uint16_t', 'u16'],
116
+ ['uint32_t', 'u32'],
117
+ ['uint64_t', 'u64'],
118
+ # Size type (platform-independent)
119
+ ['size_t', 'size'],
120
+ # Floating point types
121
+ ['float', 'f32'],
122
+ ['double', 'f'],
123
+ ['long double', 'ld'],
124
+ # Other types
125
+ ['bool', 'bool'],
126
+ ['std::string', 's'],
127
+ ['std::__cxx11::basic_string<char>', 's'],
128
+ ['std::basic_string<char>', 's'],
129
+ ['basic_string<char>', 's'],
130
+ ['char *', 's'],
131
+ ['const char *', 's'],
132
+ ]).freeze
133
+
134
+ # std:: types that Rice converts to native Ruby types (no Rice wrapper exists).
135
+ # string→String, string_view→String, complex→Complex, monostate→NilClass, tuple→Array.
136
+ # Checked by declaration spelling (not qualified_name) to avoid inline namespace issues
137
+ # (e.g., std::__cxx11::basic_string on libstdc++).
138
+ RICE_NATIVE_TYPES = Set.new(%w[basic_string basic_string_view complex monostate tuple]).freeze
139
+
140
+ FUNDAMENTAL_TYPES = [
141
+ :type_void, :type_bool,
142
+ :type_char_u, :type_uchar, :type_char16, :type_char32, :type_char_s,
143
+ :type_schar, :type_wchar,
144
+ :type_short, :type_ushort,
145
+ :type_int, :type_uint,
146
+ :type_long, :type_ulong,
147
+ :type_longlong, :type_ulonglong,
148
+ :type_int128, :type_uint128,
149
+ :type_float, :type_double, :type_longdouble,
150
+ :type_float128, :type_float16,
151
+ :type_nullptr
152
+ ].freeze
153
+
154
+ # Directory containing the ERB templates used by the Rice generator.
155
+ def self.template_dir
156
+ __dir__
157
+ end
158
+
159
+ # Create the main generator plus the extracted helpers that own naming,
160
+ # qualification, template resolution, and signature construction.
161
+ def initialize(inputter, outputter, config)
162
+ super(inputter, outputter, config)
163
+ @include_header = config[:include]
164
+ @init_names = Hash.new
165
+ @namespaces = Set.new
166
+ @classes = Hash.new # Maps cruby_name -> C++ type for Data_Type<T> declarations
167
+ @reference_qualifier = ReferenceQualifier.new
168
+ @type_index = TypeIndex.new
169
+ @type_speller = TypeSpeller.new(type_index: @type_index)
170
+ @auto_generated_bases = Set.new
171
+ @symbols = RubyBindgen::Symbols.new(config[:symbols] || {})
172
+ @export_macros = config[:export_macros] || []
173
+ @version_check = config[:version_check]
174
+ raise ArgumentError, "version_check is required when symbols.versions is non-empty" if @symbols.has_versions? && !@version_check
175
+
176
+ # Build naming tables: merge operator defaults with user config
177
+ symbols_config = config[:symbols] || {}
178
+ rename_types = RubyBindgen::NameMapper.from_config(symbols_config[:rename_types] || [])
179
+ user_rename_methods = RubyBindgen::NameMapper.from_config(symbols_config[:rename_methods] || [])
180
+ rename_methods = OPERATOR_MAPPINGS.merge(user_rename_methods)
181
+ @namer = RubyBindgen::Namer.new(rename_types, rename_methods, CONVERSION_TYPE_MAPPINGS)
182
+ @template_resolver = TemplateResolver.new(reference_qualifier: @reference_qualifier,
183
+ type_speller: @type_speller,
184
+ namer: @namer)
185
+ @signature_builder = SignatureBuilder.new(type_speller: @type_speller,
186
+ reference_qualifier: @reference_qualifier,
187
+ cursor_literals: CURSOR_LITERALS,
188
+ fundamental_types: FUNDAMENTAL_TYPES)
189
+ # Non-member operators grouped by target class cruby_name
190
+ @non_member_operators = Hash.new { |h, k| h[k] = [] }
191
+ @iterator_collector = IteratorCollector.new
192
+ end
193
+
194
+ # Parse the configured inputs with libclang and stream the resulting
195
+ # translation units back through this visitor.
196
+ def generate
197
+ clang_args = @config[:clang_args] || []
198
+ parser = RubyBindgen::Parser.new(@inputter, clang_args, libclang: @config[:libclang])
199
+ ::FFI::Clang::Cursor.namer = @namer
200
+ parser.generate(self)
201
+ end
202
+
203
+ # Check if a type references a skipped symbol by examining its declaration.
204
+ # Unwraps pointers/references and checks template arguments recursively.
205
+ def type_references_skipped_symbol?(type)
206
+ type = type.intrinsic_type
207
+
208
+ # Check the type's own declaration (try both non-canonical and canonical
209
+ # since dependent types like SkippedClass<T> may not resolve canonically)
210
+ [type, type.canonical].each do |t|
211
+ decl = t.declaration
212
+ next if decl.kind == :cursor_no_decl_found
213
+ return true if @symbols.skip?(decl)
214
+ end
215
+
216
+ # For dependent/unexposed types where no declaration is found (e.g., SkippedClass<T>
217
+ # inside a class template), fall back to checking the type spelling
218
+ if type.declaration.kind == :cursor_no_decl_found && type.canonical.declaration.kind == :cursor_no_decl_found
219
+ return true if @symbols.skip_spelling?(type.spelling)
220
+ end
221
+
222
+ # Check template arguments recursively
223
+ if type.num_template_arguments > 0
224
+ type.num_template_arguments.times do |i|
225
+ arg_type = type.template_argument_type(i)
226
+ next if arg_type.kind == :type_invalid
227
+ return true if type_references_skipped_symbol?(arg_type)
228
+ end
229
+ end
230
+
231
+ false
232
+ end
233
+
234
+ # Check if any parameter type of a callable references a skipped symbol.
235
+ def has_skipped_param_type?(cursor)
236
+ (0...cursor.type.args_size).any? do |i|
237
+ type_references_skipped_symbol?(cursor.type.arg_type(i))
238
+ end
239
+ end
240
+
241
+ # Skip rvalue-reference parameters only when the underlying type is
242
+ # genuinely move-only. Copyable types such as std::vector<T> or T&&
243
+ # assignment operators compile and should remain available.
244
+ def has_unsupported_rice_param_type?(cursor)
245
+ (0...cursor.type.args_size).any? do |i|
246
+ type = cursor.type.arg_type(i)
247
+ unsupported_rice_rvalue_ref_type?(type)
248
+ end
249
+ end
250
+
251
+ # Check if the return type of a callable references a skipped symbol.
252
+ def has_skipped_return_type?(cursor)
253
+ type_references_skipped_symbol?(cursor.type.result_type)
254
+ end
255
+
256
+ # Check if a cursor should be skipped based on symbols config.
257
+ # Adds Rice-specific template-argument matching on top of basic lookup.
258
+ def skip_symbol?(cursor)
259
+ return true if @symbols.skip?(cursor)
260
+
261
+ # Check if any template argument type references a skipped symbol
262
+ type_references_skipped_symbol?(cursor.type)
263
+ end
264
+
265
+ # Rice's std::function adapter is not reliable once callback signatures
266
+ # involve references or nested callbacks which themselves do. Skip those
267
+ # attrs instead of emitting uncompilable wrappers.
268
+ def unsupported_rice_attribute_type?(type)
269
+ type.reference?
270
+ end
271
+
272
+ # Preserve rvalue-reference bindings for copyable types. The only cases we
273
+ # still need to suppress here are move-only sinks such as unique_ptr&&
274
+ # where Rice has no usable from_ruby conversion.
275
+ def unsupported_rice_rvalue_ref_type?(type)
276
+ return false unless type.kind == :type_rvalue_ref
277
+
278
+ pointee = type.non_reference_type
279
+ move_only_std_type?(pointee) || !pointee.copyable?
280
+ end
281
+
282
+ def move_only_std_type?(type)
283
+ canonical_spelling = type.canonical.spelling
284
+ canonical_spelling.start_with?("std::unique_ptr<")
285
+ end
286
+
287
+ def unsupported_rice_vector_element_type?(type)
288
+ type = type.non_reference_type if type.reference?
289
+ canonical = type.canonical
290
+ decl = canonical.declaration
291
+ return false if decl.kind == :cursor_no_decl_found
292
+ return false unless vector_like_type?(decl)
293
+
294
+ element_type = canonical.template_argument_type(0)
295
+ return false if element_type.nil? || element_type.kind == :type_invalid
296
+
297
+ !rice_equality_supported?(element_type)
298
+ end
299
+
300
+ def vector_like_type?(decl)
301
+ decl.spelling == "vector" || decl.qualified_name == "std::vector"
302
+ end
303
+
304
+ def variant_like_type?(decl)
305
+ decl.spelling == "variant" || decl.qualified_name == "std::variant"
306
+ end
307
+
308
+ def rice_equality_supported?(type)
309
+ type = type.non_reference_type if type.reference?
310
+ type = type.canonical
311
+
312
+ return true if FUNDAMENTAL_TYPES.include?(type.kind) || type.kind == :type_enum
313
+ return true if [:type_pointer, :type_member_pointer].include?(type.kind)
314
+
315
+ decl = type.declaration
316
+ return true if decl.kind == :cursor_no_decl_found
317
+ return true if comparable_std_type?(decl)
318
+
319
+ if variant_like_type?(decl)
320
+ return (0...type.num_template_arguments).all? do |i|
321
+ arg_type = type.template_argument_type(i)
322
+ next true if arg_type.kind == :type_invalid
323
+
324
+ rice_equality_supported?(arg_type)
325
+ end
326
+ end
327
+
328
+ has_equality_operator?(decl)
329
+ end
330
+
331
+ def comparable_std_type?(decl)
332
+ ["basic_string", "string", "monostate"].include?(decl.spelling) ||
333
+ ["std::string", "std::monostate"].include?(decl.qualified_name)
334
+ end
335
+
336
+ def has_equality_operator?(decl)
337
+ return true if decl.find_by_kind(false, :cursor_cxx_method).any? do |method|
338
+ method.spelling == "operator==" && method.type.args_size == 1
339
+ end
340
+
341
+ @translation_unit_cursor.find_by_kind(true, :cursor_function, :cursor_function_template).any? do |function|
342
+ next false unless function.spelling == "operator=="
343
+ next false unless function.type.args_size == 2
344
+
345
+ arg_declarations = 2.times.map do |index|
346
+ function.type.arg_type(index).intrinsic_type.canonical.declaration
347
+ end
348
+
349
+ arg_declarations.all? do |arg_decl|
350
+ arg_decl.kind != :cursor_no_decl_found &&
351
+ (arg_decl == decl || arg_decl.qualified_name == decl.qualified_name)
352
+ end
353
+ end
354
+ end
355
+
356
+ def unsupported_rice_callback_type?(type)
357
+ type = type.non_reference_type if type.reference?
358
+ canonical = type.canonical
359
+ decl = canonical.declaration
360
+ return false if decl.kind == :cursor_no_decl_found
361
+ return false unless decl.qualified_name == "std::function"
362
+
363
+ callback_signature_unsupported?(canonical.template_argument_type(0))
364
+ end
365
+
366
+ def callback_signature_unsupported?(type)
367
+ return false if type.nil? || type.kind == :type_invalid
368
+ return false unless [:type_function_proto, :type_function_no_proto].include?(type.kind)
369
+
370
+ return true if type.result_type.reference?
371
+
372
+ type.arg_types.any? do |arg_type|
373
+ arg_type.reference? || unsupported_rice_callback_type?(arg_type)
374
+ end
375
+ end
376
+
377
+ def implicit_default_constructor_available?(cursor)
378
+ cursor.find_by_kind(false, :cursor_field_decl).none? do |field|
379
+ field.type.reference?
380
+ end
381
+ end
382
+
383
+ # Reset any per-run caches before parsing begins.
384
+ def visit_start
385
+ # Clear caches from previous runs
386
+ @type_speller.clear
387
+ end
388
+
389
+ def visit_parse_error(_path, relative_path, error)
390
+ warn "Warning: skipping #{relative_path} because it could not be parsed"
391
+ warn error.message
392
+ end
393
+
394
+ # Emit the shared include header and optional project wrapper once all
395
+ # translation units have been processed.
396
+ def visit_end
397
+ create_rice_include_header
398
+ create_project_files
399
+ end
400
+
401
+ # Returns the path to the Rice include header (user-specified or auto-generated)
402
+ def rice_include_header
403
+ @include_header || "#{@project || 'rice'}_include.hpp"
404
+ end
405
+
406
+ # Compute the .ipp path for a template defined in a different file.
407
+ def ipp_path_for_cursor(cursor)
408
+ template_file = cursor.file_location.file
409
+ relative = Pathname.new(template_file).relative_path_from(Pathname.new(@inputter.base_path)).to_s
410
+ File.join(File.dirname(relative), "#{File.basename(relative, '.*')}-rb.ipp")
411
+ end
412
+
413
+ # Generate default Rice include header if user didn't specify one.
414
+ # If the file already exists on disk, preserve it so user customizations are not lost.
415
+ def create_rice_include_header
416
+ return if @include_header # User specified their own header
417
+
418
+ header_path = rice_include_header
419
+ output_path = self.outputter.output_path(header_path)
420
+ if File.exist?(output_path)
421
+ STDOUT << " Preserving: " << header_path << "\n"
422
+ return
423
+ end
424
+
425
+ STDOUT << " Writing: " << header_path << "\n"
426
+ content = render_template("rice_include.hpp")
427
+ self.outputter.write(header_path, content)
428
+ end
429
+
430
+ # Render one translation unit into its generated `-rb.hpp`, `-rb.cpp`, and
431
+ # optional `-rb.ipp` outputs.
432
+ def visit_translation_unit(translation_unit, path, relative_path)
433
+ @namespaces.clear
434
+ @classes.clear
435
+ @auto_generated_bases.clear
436
+ @non_member_operators.clear
437
+ @iterator_collector.clear
438
+ cursor = translation_unit.cursor
439
+ @translation_unit_cursor = cursor
440
+ @type_speller.printing_policy = cursor.printing_policy
441
+
442
+ # Build lookups for typedef resolution and simple-name qualification.
443
+ @type_index.build!(cursor)
444
+
445
+ # Figure out relative paths for generated header and cpp file
446
+ @basename = "#{File.basename(relative_path, ".*")}-rb"
447
+ @relative_dir = File.dirname(relative_path)
448
+ rice_header = File.join(@relative_dir, "#{@basename}.hpp")
449
+ rice_cpp = File.join(@relative_dir, "#{@basename}.cpp")
450
+
451
+ # Track init names - use relative path to avoid conflicts (e.g., core/version vs dnn/version)
452
+ path_parts = Pathname.new(relative_path).each_filename.to_a
453
+ path_parts.shift if path_parts.length >= 2 # Remove top-level directory (e.g., opencv2)
454
+ filename = Pathname.new(path_parts.pop).sub_ext('').to_s.camelize
455
+ dir_part = path_parts.map(&:camelize).join('_')
456
+ init_name = dir_part.empty? ? "Init_#{filename}" : "Init_#{dir_part}_#{filename}"
457
+ @init_names[rice_header] = init_name
458
+
459
+ @includes = Set.new
460
+ @includes << "#include <#{relative_path}>"
461
+ @includes << "#include \"#{@basename}.hpp\""
462
+
463
+ class_templates, has_builders = render_class_templates(cursor)
464
+ content = render_children(cursor, :indentation => 2)
465
+
466
+ # Render non-member operators grouped by class
467
+ non_member_ops = render_non_member_operators
468
+ unless non_member_ops.empty?
469
+ content = content + "\n\n " + non_member_ops
470
+ end
471
+
472
+ # Generate .ipp file if builders exist (for reusability without duplicate Init symbols)
473
+ rice_ipp = nil
474
+ if has_builders
475
+ rice_ipp = File.join(File.dirname(relative_path), "#{@basename}.ipp")
476
+ STDOUT << " Writing: " << rice_ipp << "\n"
477
+ ipp_content = render_cursor(cursor, "translation_unit.ipp",
478
+ :class_templates => class_templates)
479
+ self.outputter.write(rice_ipp, ipp_content)
480
+ end
481
+
482
+ # Render C++ file
483
+ STDOUT << " Writing: " << rice_cpp << "\n"
484
+ content = render_cursor(cursor, "translation_unit.cpp",
485
+ :class_templates => class_templates,
486
+ :content => content,
487
+ :includes => @includes,
488
+ :init_name => init_name,
489
+ :rice_header => rice_header,
490
+ :incomplete_iterators => @iterator_collector.incomplete_iterators,
491
+ :rice_ipp => rice_ipp ? File.basename(rice_ipp) : nil)
492
+ self.outputter.write(rice_cpp, content)
493
+
494
+ # Render header file
495
+ STDOUT << " Writing: " << rice_header << "\n"
496
+ # Compute relative path from translation unit directory to the include header
497
+ relative_include = Pathname.new(rice_include_header).relative_path_from(File.dirname(relative_path)).to_s
498
+ content = render_cursor(cursor, "translation_unit.hpp",
499
+ :init_name => init_name,
500
+ :rice_include_header => relative_include)
501
+ self.outputter.write(rice_header, content)
502
+ end
503
+
504
+ # Render a public, callable constructor into the Rice chain for its class.
505
+ def visit_constructor(cursor)
506
+ # Do not process class constructors defined outside of the class definition
507
+ return if cursor.lexical_parent != cursor.semantic_parent
508
+
509
+ # Do not process deleted constructors
510
+ return if cursor.deleted?
511
+
512
+ # Skip deprecated constructors (they may not be exported from library)
513
+ return if cursor.availability == :deprecated
514
+
515
+ # Skip explicitly listed constructors
516
+ return if skip_symbol?(cursor)
517
+
518
+ # Do not process move constructors
519
+ return if cursor.move_constructor?
520
+
521
+ # Do not construct abstract classes
522
+ return if cursor.semantic_parent.abstract?
523
+
524
+ # Skip constructors that take skipped types as parameters
525
+ return if has_skipped_param_type?(cursor)
526
+ return if has_unsupported_rice_param_type?(cursor)
527
+
528
+ signature = @signature_builder.constructor_signature(cursor)
529
+ args = @signature_builder.arguments(cursor)
530
+
531
+ return unless signature
532
+
533
+ self.render_cursor(cursor, "constructor",
534
+ :signature => signature, :args => args)
535
+ end
536
+
537
+ # Render a class or struct, including child members, anonymous enum
538
+ # constants, embedded types, and any auto-generated template bases needed
539
+ # before the class itself can be registered.
540
+ def visit_class_decl(cursor)
541
+ return if skip_namespace_forward_declaration?(cursor)
542
+
543
+ # Skip explicitly listed symbols
544
+ return if skip_symbol?(cursor)
545
+
546
+ # Skip anonymous types with no definer (no typedef, field, or variable name).
547
+ # These are unnameable in C++ and cannot be meaningfully bound.
548
+ return if cursor.anonymous? && !cursor.anonymous_definer
549
+
550
+ result = Hash.new { |h, k| h[k] = [] }
551
+
552
+ # Determine containing module
553
+ under = find_under(cursor)
554
+
555
+ # Is there a base class?
556
+ base = nil
557
+ auto_generated_base = ""
558
+ base_specifier = cursor.find_first_by_kind(false, :cursor_cxx_base_specifier)
559
+ if base_specifier
560
+ # Use canonical spelling for fully qualified type name with namespaces
561
+ base = base_specifier.type.canonical.spelling
562
+
563
+ # Check if base is a template instantiation that needs to be auto-generated
564
+ if base.include?('<') && !@auto_generated_bases.include?(base)
565
+ auto_generated_base = auto_generate_template_base_for_class(base_specifier, base, under)
566
+ end
567
+ end
568
+
569
+ # Visit children
570
+ versions = visit_children(cursor,
571
+ exclude_kinds: Set[:cursor_class_decl, :cursor_struct, :cursor_enum_decl, :cursor_typedef_decl])
572
+
573
+ # Are there any constructors? If not, C++ will define one implicitly
574
+ # (but not for incomplete/opaque types which can't be instantiated)
575
+ constructors = cursor.find_by_kind(false, :cursor_constructor)
576
+ if !cursor.abstract? &&
577
+ !cursor.opaque_declaration? &&
578
+ constructors.none? &&
579
+ implicit_default_constructor_available?(cursor)
580
+ versions[nil].unshift(self.render_template("constructor",
581
+ :cursor => cursor,
582
+ :signature => @signature_builder.constructor_signature(cursor),
583
+ :args => []))
584
+ end
585
+
586
+ # Add anonymous enum constants to the class chain (with per-constant versioning)
587
+ cursor.find_by_kind(false, :cursor_enum_decl) do |child_cursor|
588
+ next if child_cursor.private? || child_cursor.protected?
589
+ next unless child_cursor.anonymous?
590
+ constant_versions = visit_children(child_cursor)
591
+ constant_versions.each do |version, lines|
592
+ versions[version].concat(lines.map(&:strip))
593
+ end
594
+ end
595
+
596
+ children_content = merge_children(versions, indentation: 2, chain: true, terminate: true, strip: true)
597
+
598
+ # Collect forward-declared (incomplete) inner classes
599
+ # They must be registered with Rice before the parent class methods use them
600
+ incomplete_classes = []
601
+ cursor.find_by_kind(false, :cursor_class_decl, :cursor_struct) do |child_cursor|
602
+ next if child_cursor.private? || child_cursor.protected?
603
+ next unless child_cursor.opaque_declaration?
604
+ definition = child_cursor.definition
605
+ if definition &&
606
+ ![:cursor_invalid_file, :cursor_no_decl_found].include?(definition.kind) &&
607
+ !definition.opaque_declaration? &&
608
+ translation_unit_file?(definition)
609
+ next
610
+ end
611
+ incomplete_classes << visit_incomplete_class(child_cursor, cursor)
612
+ end
613
+ incomplete_classes_content = merge_children({ nil => incomplete_classes })
614
+
615
+ # Auto-instantiate any class templates used as parameter types
616
+ auto_instantiated = auto_instantiate_parameter_templates(cursor, under)
617
+ result[nil] << auto_instantiated unless auto_instantiated.empty?
618
+
619
+ # Render class
620
+ cpp_type = @type_speller.qualified_class_name(cursor)
621
+ raw_class_name = cursor.type.spelling.split("::").last
622
+ ruby_class_name = @namer.apply_rename_types(raw_class_name, raw_class_name.camelize)
623
+ has_incomplete_classes = !incomplete_classes_content.to_s.empty?
624
+ @classes[cursor.cruby_name] = cpp_type
625
+ result[nil] << self.render_cursor(cursor, "class", :under => under, :base => base,
626
+ :auto_generated_base => auto_generated_base,
627
+ :incomplete_classes => incomplete_classes_content,
628
+ :children => children_content,
629
+ :cpp_type => cpp_type,
630
+ :ruby_class_name => ruby_class_name,
631
+ :has_incomplete_classes => has_incomplete_classes)
632
+
633
+ # Alias each_const to each if the class only has const iterators
634
+ if @iterator_collector.each_const_only?(cursor.cruby_name)
635
+ result[nil] << render_template("iterator_alias", :cruby_name => cursor.cruby_name)
636
+ end
637
+
638
+ # Define any complete embedded classes and structs
639
+ cursor.find_by_kind(false, :cursor_class_decl, :cursor_struct) do |child_cursor|
640
+ next if child_cursor.private? || child_cursor.protected?
641
+ next if child_cursor.forward_declaration?
642
+ next if child_cursor.opaque_declaration?
643
+ content = visit_class_decl(child_cursor)
644
+ next unless content
645
+ version = @symbols.version(child_cursor)
646
+ result[version] << content
647
+ end
648
+
649
+ # Define any named embedded enums (anonymous enums are chained above)
650
+ cursor.find_by_kind(false, :cursor_enum_decl) do |child_cursor|
651
+ next if child_cursor.private? || child_cursor.protected?
652
+ next if child_cursor.anonymous?
653
+ version = @symbols.version(child_cursor)
654
+ result[version] << visit_enum_decl(child_cursor)
655
+ end
656
+
657
+ merge_children(result)
658
+ end
659
+ alias :visit_struct :visit_class_decl
660
+
661
+ # Scan a class's methods for class template parameters that need auto-instantiation.
662
+ def auto_instantiate_parameter_templates(cursor, under)
663
+ result = []
664
+
665
+ cursor.each(false) do |child, _|
666
+ next unless [:cursor_cxx_method, :cursor_constructor].include?(child.kind)
667
+ next if child.private? || child.protected?
668
+
669
+ child.num_arguments.times do |i|
670
+ param = child.argument(i)
671
+ type = param.type.intrinsic_type
672
+
673
+ # Skip if not a template instantiation or is from a system header (std::, etc.)
674
+ next unless type.num_template_arguments > 0
675
+ next if type.canonical.declaration.location.in_system_header?
676
+
677
+ # Find the class template declaration semantically so alias parameters
678
+ # like `using AliasContainer = Container<Item>` still auto-instantiate
679
+ # the underlying `Container<Item>` specialization.
680
+ decl, instantiated_type, instantiated_type_source = parameter_template_instantiation(type, param)
681
+ next unless decl && decl.kind == :cursor_class_template
682
+ next unless decl.location.file == cursor.location.file
683
+
684
+ # Auto-instantiate if no typedef exists
685
+ instantiated_type = @type_speller.qualify_class_static_members(instantiated_type, cursor)
686
+ next if @type_index.typedef_for(instantiated_type)
687
+
688
+ code = auto_instantiate_template(decl, instantiated_type, instantiated_type_source, under)
689
+ result << code unless code.empty?
690
+ end
691
+ end
692
+
693
+ merge_children({ nil => result })
694
+ end
695
+
696
+ # Resolve the actual class template being instantiated for a parameter type.
697
+ # This prefers semantic specialization data so aliases such as
698
+ # `using AliasContainer = Container<Item>` still resolve to `Container`.
699
+ def parameter_template_instantiation(type, param)
700
+ declaration = type.declaration
701
+ specialized_template = declaration.specialized_template
702
+ unless specialized_template.kind == :cursor_invalid_file
703
+ return [specialized_template, @type_speller.type_spelling(type.unqualified_type), type]
704
+ end
705
+
706
+ canonical_type = type.canonical
707
+ canonical_declaration = canonical_type.declaration
708
+ specialized_template = canonical_declaration.specialized_template
709
+ unless specialized_template.kind == :cursor_invalid_file
710
+ return [specialized_template, @type_speller.type_spelling(canonical_type.unqualified_type), canonical_type]
711
+ end
712
+
713
+ template_ref = param.find_first_by_kind(true, :cursor_template_ref)
714
+ return [nil, nil, type] unless template_ref
715
+
716
+ [template_ref.referenced, @type_speller.type_spelling(type.unqualified_type), type]
717
+ end
718
+
719
+ # Visit a forward-declared (incomplete) inner class.
720
+ # These need to be registered with Rice so that types like Ptr<Impl> work.
721
+ # Must be registered BEFORE the parent class methods that use them (smart pointer issue).
722
+ def visit_incomplete_class(cursor, parent_cursor)
723
+ # Skip if already defined
724
+ return "" if @classes.key?(cursor.cruby_name)
725
+
726
+ @classes[cursor.cruby_name] = cursor.qualified_name
727
+ render_cursor(cursor, "incomplete_class", :under => parent_cursor)
728
+ end
729
+
730
+ # Get base class spelling from a cursor's base specifier.
731
+ # Handles both template and non-template base classes.
732
+ # Returns nil if no base class exists.
733
+ def get_base_spelling(cursor)
734
+ base_specifier = cursor.find_first_by_kind(false, :cursor_cxx_base_specifier)
735
+ return nil unless base_specifier
736
+
737
+ base_declaration = base_specifier.type&.declaration
738
+ return nil unless base_declaration && base_declaration.kind != :cursor_no_decl_found
739
+
740
+ specialized_template = base_declaration.specialized_template
741
+ base_cursor = specialized_template.kind == :cursor_invalid_file ? base_declaration : specialized_template
742
+
743
+ # Skip system-header base classes (e.g., std::shared_ptr)
744
+ return nil if base_cursor.location.in_system_header?
745
+
746
+ @template_resolver.resolve_base_specifier_spelling(base_specifier)
747
+ end
748
+
749
+ # Render the reusable `_instantiate` helper for a class template so typedefs
750
+ # and alias specializations can bind that template later.
751
+ def visit_class_template_builder(cursor)
752
+ children_content = render_children(cursor,
753
+ only_kinds: [:cursor_cxx_method, :cursor_constructor, :cursor_field_decl, :cursor_variable,
754
+ :cursor_enum_decl, :cursor_conversion_function],
755
+ indentation: 4, chain: true,
756
+ terminate: true, strip: true)
757
+
758
+ base_spelling = get_base_spelling(cursor)
759
+
760
+ template_parameter_kinds = [:cursor_template_type_parameter,
761
+ :cursor_non_type_template_parameter,
762
+ :cursor_template_template_parameter]
763
+
764
+ raw_template_parameters = cursor.find_by_kind(false, *template_parameter_kinds)
765
+
766
+ # Filter out unnamed template parameters (e.g., `typename = void` default params)
767
+ # — they have empty spelling and produce invalid C++ like `<T, >`
768
+ template_parameters = raw_template_parameters.reject { |p| p.spelling.empty? }
769
+ return if template_parameters.empty? && raw_template_parameters.any?
770
+ template_signature = template_parameters.map { |template_parameter| @template_resolver.template_parameter_signature(template_parameter) }
771
+ .join(", ")
772
+
773
+ # Build fully qualified type using template params (e.g., Tests::Matrix<T, Rows, Columns>)
774
+ param_names = template_parameters.map { |template_parameter| @template_resolver.template_parameter_argument(template_parameter) }
775
+ .join(", ")
776
+ fully_qualified_type = "#{cursor.qualified_name}<#{param_names}>"
777
+
778
+ # Determine containing module
779
+ under = find_under(cursor)
780
+
781
+ # Render class
782
+ result = Array.new
783
+ result << self.render_cursor(cursor, "class_template", :under => under,
784
+ :template_signature => template_signature,
785
+ :fully_qualified_type => fully_qualified_type,
786
+ :base_spelling => base_spelling,
787
+ :children => children_content)
788
+
789
+ merge_children({ nil => result })
790
+ end
791
+
792
+ # Find the nearest enclosing module (namespace, class, or struct), skipping
793
+ # inline namespaces which don't map to Ruby modules.
794
+ def find_under(cursor)
795
+ cursor.ancestors_by_kind(:cursor_class_decl, :cursor_struct, :cursor_namespace)
796
+ .find { |a| a.kind != :cursor_namespace || !a.inline_namespace? }
797
+ end
798
+
799
+ # Check if cursor has one of the required export macros in its source text
800
+ # Used to filter out non-exported functions (e.g., only include CV_EXPORTS functions)
801
+ def has_export_macro?(cursor)
802
+ return true if @export_macros.empty?
803
+
804
+ source_text = cursor.extent.text
805
+ return false if source_text.nil?
806
+ @export_macros.any? { |macro| source_text.include?(macro) }
807
+ end
808
+
809
+ ITERATOR_METHODS = ["begin", "end", "cbegin", "cend", "rbegin", "rend", "crbegin", "crend"].freeze
810
+
811
+ # Common skip checks for functions and methods
812
+ def skip_callable?(cursor)
813
+ skip_symbol?(cursor) ||
814
+ cursor.availability == :deprecated ||
815
+ cursor.type.variadic?
816
+ end
817
+
818
+ # Render a class method, including special handling for iterator adapters
819
+ # and mutable `operator[]` setter support.
820
+ def visit_cxx_method(cursor)
821
+ # Do not process method definitions outside of classes (because we already processed them)
822
+ return if cursor.lexical_parent != cursor.semantic_parent
823
+ return if skip_callable?(cursor)
824
+ return if has_skipped_param_type?(cursor)
825
+ return if has_unsupported_rice_param_type?(cursor)
826
+ return if has_skipped_return_type?(cursor)
827
+
828
+ # Is this an iterator?
829
+ if ITERATOR_METHODS.include?(cursor.spelling)
830
+ return visit_cxx_iterator_method(cursor)
831
+ end
832
+
833
+ signature = @signature_builder.method_signature(cursor)
834
+
835
+ result = Array.new
836
+
837
+ name = cursor.ruby_name
838
+ args = @signature_builder.arguments(cursor)
839
+
840
+ # Check if return type should use ReturnBuffer
841
+ return_buffer = @signature_builder.buffer_type?(cursor.result_type)
842
+
843
+ is_template = cursor.semantic_parent.kind == :cursor_class_template
844
+ qualified_parent = @type_speller.qualified_display_name(cursor.semantic_parent)
845
+ result << self.render_cursor(cursor, "cxx_method",
846
+ :name => name,
847
+ :is_template => is_template,
848
+ :signature => signature,
849
+ :args => args,
850
+ :return_buffer => return_buffer,
851
+ :qualified_parent => qualified_parent)
852
+
853
+ # Special handling for implementing #[](index, value)
854
+ if cursor.spelling == "operator[]" && cursor.result_type.kind == :type_lvalue_ref &&
855
+ !cursor.result_type.non_reference_type.const_qualified? && !cursor.const?
856
+ index_param = cursor.find_by_kind(false, :cursor_parm_decl).first
857
+ index_type = @type_speller.type_spelling(cursor.type.arg_type(0))
858
+ index_name = index_param&.spelling.to_s.empty? ? "index" : index_param.spelling
859
+ value_type = @type_speller.type_spelling(cursor.result_type)
860
+ result << self.render_cursor(cursor, "operator[]",
861
+ :name => name,
862
+ :index_type => index_type,
863
+ :index_name => index_name,
864
+ :qualified_parent => qualified_parent,
865
+ :value_type => value_type)
866
+ end
867
+ result
868
+ end
869
+
870
+ def skip_namespace_forward_declaration?(cursor)
871
+ return false unless cursor.kind == :cursor_class_decl
872
+ return false unless cursor.opaque_declaration?
873
+ return false if [:cursor_class_decl, :cursor_struct].include?(cursor.semantic_parent.kind)
874
+
875
+ definition = cursor.definition
876
+ return false if [:cursor_invalid_file, :cursor_no_decl_found].include?(definition.kind)
877
+ return false if definition.opaque_declaration?
878
+
879
+ translation_unit_file?(definition)
880
+ end
881
+
882
+ # Generates Rice define_iterator calls for C++ iterator methods.
883
+ # In C++, cbegin/crbegin can be called on non-const objects but return
884
+ # const iterators while const begin/rbegin require a const receiver.
885
+ # Ruby has no such distinction, so the collector skips cbegin/crbegin
886
+ # to avoid emitting duplicate each_const / each_reverse_const methods.
887
+ def visit_cxx_iterator_method(cursor)
888
+ iterator_name = @iterator_collector.record(cursor)
889
+ return unless iterator_name
890
+
891
+ signature = @signature_builder.method_signature(cursor)
892
+ return unless signature
893
+
894
+ begin_method = cursor.spelling
895
+ end_method = begin_method.sub("begin", "end")
896
+ is_template = cursor.semantic_parent.kind == :cursor_class_template
897
+ qualified_parent = @type_speller.qualified_display_name(cursor.semantic_parent)
898
+
899
+ self.render_cursor(cursor, "cxx_iterator_method",
900
+ :name => iterator_name,
901
+ :begin_method => begin_method,
902
+ :end_method => end_method,
903
+ :signature => signature,
904
+ :is_template => is_template,
905
+ :qualified_parent => qualified_parent)
906
+ end
907
+
908
+ # Render a conversion operator such as `operator bool()` or `operator T*()`.
909
+ # Template-parameter conversions that cannot produce stable Ruby names are skipped.
910
+ def visit_conversion_function(cursor)
911
+ # For now only deal with member functions
912
+ return unless CURSOR_CLASSES.include?(cursor.lexical_parent.kind)
913
+
914
+ return if skip_callable?(cursor)
915
+
916
+ return unless cursor.type.args_size == 0
917
+ return if has_skipped_return_type?(cursor)
918
+
919
+ result_type = cursor.type.result_type
920
+
921
+ # Skip "safe bool idiom" conversion operators from pre-C++11.
922
+ # These are typedefs to member function pointers used before explicit operator bool().
923
+ # Pattern: typedef void (Class::*bool_type)() const; operator bool_type() const;
924
+ if result_type.declaration.kind == :cursor_typedef_decl
925
+ canonical = result_type.canonical
926
+ if canonical.kind == :type_member_pointer
927
+ # Check if the pointee is a function type
928
+ pointee = canonical.pointee
929
+ if pointee.kind == :type_function_proto || pointee.kind == :type_function_no_proto
930
+ return
931
+ end
932
+ end
933
+ end
934
+
935
+ result_type_spelling = @type_speller.type_spelling(result_type)
936
+ is_const = cursor.const?
937
+
938
+ # For class templates, the result type may contain "type-parameter-X_Y" which
939
+ # generates invalid Ruby method names. Use generic names instead.
940
+ if cursor.semantic_parent.kind == :cursor_class_template &&
941
+ result_type.canonical.spelling.include?("type-parameter-")
942
+ # Determine if this is a pointer conversion
943
+ if result_type.kind == :type_pointer
944
+ ruby_name = is_const ? "to_const_ptr" : "to_ptr"
945
+ else
946
+ # Skip other template parameter conversions for now
947
+ return
948
+ end
949
+ else
950
+ ruby_name = cursor.ruby_name
951
+ end
952
+
953
+ qualified_parent = @type_speller.qualified_display_name(cursor.semantic_parent)
954
+ self.render_cursor(cursor, "conversion_function",
955
+ :ruby_name => ruby_name, :result_type => result_type_spelling,
956
+ :is_const => is_const,
957
+ :qualified_parent => qualified_parent)
958
+ end
959
+
960
+ # Render a named enum as a Rice enum, or flatten anonymous enum constants
961
+ # into the surrounding class/namespace output.
962
+ def visit_enum_decl(cursor)
963
+ return if CURSOR_CLASSES.include?(cursor.semantic_parent.kind) && !cursor.public?
964
+ return if !cursor.anonymous? && skip_symbol?(cursor)
965
+
966
+ if cursor.anonymous? && CURSOR_CLASSES.include?(cursor.semantic_parent.kind)
967
+ # Anonymous enum inside a class — return constants as chainable strings
968
+ versions = visit_children(cursor)
969
+ return versions.values.flatten.map(&:strip)
970
+ elsif cursor.anonymous?
971
+ # Anonymous enum at namespace/TU level — return standalone constant definitions
972
+ return render_children(cursor, strip: true)
973
+ end
974
+
975
+ under = find_under(cursor)
976
+ children = render_children(cursor, indentation: 2, chain: true, terminate: true, strip: true)
977
+ self.render_cursor(cursor, "enum_decl", :under => under, :children => children)
978
+ end
979
+
980
+ # Render one enum constant, preserving whether it came from an anonymous
981
+ # enum and whether that enum lives inside a class scope.
982
+ def visit_enum_constant_decl(cursor)
983
+ return if skip_symbol?(cursor)
984
+ enum_parent = cursor.semantic_parent
985
+ enum_scope = enum_parent.semantic_parent
986
+ anonymous_parent = enum_parent.anonymous?
987
+ anonymous_class_scope = anonymous_parent &&
988
+ [:cursor_class_decl, :cursor_struct, :cursor_class_template].include?(enum_scope.kind)
989
+ qualified_name = "#{@type_speller.qualified_display_name(enum_scope)}::#{cursor.spelling}"
990
+
991
+ self.render_cursor(cursor, "enum_constant_decl",
992
+ :anonymous_parent => anonymous_parent,
993
+ :anonymous_class_scope => anonymous_class_scope,
994
+ :owner_cruby_name => enum_scope.cruby_name,
995
+ :qualified_name => qualified_name,
996
+ :value_name => cursor.qualified_display_name)
997
+ end
998
+
999
+ # Render a free function or non-member operator that survives the symbol,
1000
+ # export-macro, and parameter/return-type filters.
1001
+ def visit_function(cursor)
1002
+ # Skip `= delete`d free functions. clang_CXXMethod_isDeleted only
1003
+ # applies to class methods, so we use clang_getCursorAvailability,
1004
+ # which reports :not_available for deleted free functions.
1005
+ return if cursor.availability == :not_available
1006
+ # Can't return arrays in C++
1007
+ return if cursor.type.result_type.is_a?(::FFI::Clang::Types::Array)
1008
+ return if skip_callable?(cursor)
1009
+ return if has_skipped_param_type?(cursor)
1010
+ return if has_unsupported_rice_param_type?(cursor)
1011
+ return if has_skipped_return_type?(cursor)
1012
+ return unless has_export_macro?(cursor)
1013
+
1014
+ if cursor.spelling.start_with?('operator') && !cursor.spelling.match?(/^operator\w/)
1015
+ return self.visit_operator_non_member(cursor)
1016
+ end
1017
+
1018
+ name = cursor.ruby_name
1019
+ args = @signature_builder.arguments(cursor)
1020
+
1021
+ signature = @signature_builder.method_signature(cursor)
1022
+
1023
+ # Check if return type should use ReturnBuffer
1024
+ return_buffer = @signature_builder.buffer_type?(cursor.type.result_type)
1025
+
1026
+ under = cursor.ancestors_by_kind(:cursor_namespace)
1027
+ .find { |a| !a.inline_namespace? }
1028
+ self.render_cursor(cursor, "function",
1029
+ :under => under,
1030
+ :name => name,
1031
+ :signature => signature,
1032
+ :args => args,
1033
+ :return_buffer => return_buffer)
1034
+ end
1035
+
1036
+ # Render simple object-like macros as Ruby constants when the macro body is
1037
+ # exactly one literal token.
1038
+ def visit_macro_definition(cursor)
1039
+ tokens = cursor.translation_unit.tokenize(cursor.extent)
1040
+ return unless tokens.size == 2
1041
+ return unless tokens.tokens[0].kind == :identifier
1042
+ return unless tokens.tokens[1].kind == :literal
1043
+ return if skip_symbol?(cursor)
1044
+
1045
+ self.render_cursor(cursor, "constant",
1046
+ :name => tokens.tokens[0].spelling.upcase_first,
1047
+ :qualified_name => tokens.tokens[0].spelling)
1048
+ end
1049
+
1050
+ # Render a namespace as a Ruby module, except for inline namespaces which
1051
+ # are flattened into their enclosing namespace.
1052
+ def visit_namespace(cursor)
1053
+ # Skip anonymous namespaces - they're internal implementation details
1054
+ return if cursor.anonymous?
1055
+
1056
+ # Inline namespaces (e.g., std::__1, abseil's lts_*) should not create
1057
+ # Ruby modules — their members belong to the enclosing namespace.
1058
+ # Recurse into children without registering a new module.
1059
+ if cursor.inline_namespace?
1060
+ return self.render_children(cursor)
1061
+ end
1062
+
1063
+ result = Array.new
1064
+
1065
+ # Don't redefine a namespace twice. It doesn't matter to Ruby, but C++ wrapper
1066
+ # will break with a redefinition error:
1067
+ # Module rb_mNamespace = define_module("namespace");
1068
+ # Module rb_mNamespace = define_module("namespace");
1069
+ qualified_display_name = cursor.qualified_display_name
1070
+ unless @namespaces.include?(qualified_display_name)
1071
+ @namespaces << qualified_display_name
1072
+ under = find_under(cursor)
1073
+ result << self.render_cursor(cursor, "namespace", :under => under)
1074
+ end
1075
+
1076
+ result << self.render_children(cursor)
1077
+
1078
+ result.map { |s| s.chomp }.reject(&:empty?).join("\n\n")
1079
+ end
1080
+
1081
+ # Render a public field as a Rice attribute on its containing class.
1082
+ def visit_field_decl(cursor)
1083
+ return unless cursor.public?
1084
+ return if skip_symbol?(cursor)
1085
+ return if cursor.availability == :deprecated
1086
+ return if unsupported_rice_attribute_type?(cursor.type)
1087
+
1088
+ qualified_parent = @type_speller.qualified_display_name(cursor.semantic_parent)
1089
+ self.render_cursor(cursor, "field_decl",
1090
+ :qualified_parent => qualified_parent)
1091
+ end
1092
+
1093
+ # Record a free operator for later rendering onto the target class.
1094
+ # These are grouped and emitted after normal members so cross-file
1095
+ # `Data_Type<T>()` references can be handled in one pass.
1096
+ def visit_operator_non_member(cursor)
1097
+ # This is a stand-alone operator, such as:
1098
+ #
1099
+ # MatExpr operator + (const Mat& a, const Mat& b); # binary (2 args)
1100
+ # std::ostream& operator << (std::ostream& out, const Complex<_Tp>& c)
1101
+ # MatExpr operator ~(const Mat& m); # unary (1 arg)
1102
+ # MatExpr operator -(const Mat& m); # unary negation (1 arg)
1103
+ return if cursor.type.args_size < 1 || cursor.type.args_size > 2
1104
+
1105
+ arg0_type = cursor.type.arg_type(0).non_reference_type
1106
+ class_cursor = arg0_type.declaration
1107
+
1108
+ # Skip when the first argument is a fundamental type (e.g., double) or a
1109
+ # typedef to one (e.g., ptrdiff_t -> long long). There is no Rice wrapper
1110
+ # for these types so Data_Type<T>() would be invalid.
1111
+ return if class_cursor.kind == :cursor_no_decl_found
1112
+ return if FUNDAMENTAL_TYPES.include?(arg0_type.canonical.kind)
1113
+ # Rice already provides bitwise operators (&, |, ^, ~, <<, >>) for enums automatically
1114
+ return if class_cursor.kind == :cursor_enum_decl
1115
+
1116
+ # Skip types that Rice converts to native Ruby types (no Rice wrapper exists).
1117
+ # Note: std::vector, std::pair, etc. ARE wrapped by Rice.
1118
+ canonical_decl = arg0_type.canonical.declaration
1119
+ return if canonical_decl.kind != :cursor_no_decl_found &&
1120
+ canonical_decl.location.in_system_header? &&
1121
+ RICE_NATIVE_TYPES.include?(canonical_decl.spelling)
1122
+
1123
+ # Use the class cursor directly - operators should be attached to the actual class
1124
+ # (e.g., rb_cCvMat for cv::Mat), not to typedefs (e.g., rb_cMatND which is typedef for Mat)
1125
+ # Collect non-member operators to render grouped by class later
1126
+ @non_member_operators[class_cursor.cruby_name] << { cursor: cursor, class_cursor: class_cursor }
1127
+ nil
1128
+ end
1129
+
1130
+ # Render the queued non-member operators grouped by their target class.
1131
+ # Includes ostream-based `inspect`, unary operators, and binary operators.
1132
+ # Each branch produces a [target_cursor, rendered_line] pair; we group
1133
+ # the lines by the target class's cruby_name and emit one chained
1134
+ # `class.define(...)...` block per class.
1135
+ def render_non_member_operators
1136
+ # cpp_type carries the qualified class name for cross-file Data_Type<T>() refs.
1137
+ grouped = Hash.new { |h, k| h[k] = { lines: [], cpp_type: nil } }
1138
+
1139
+ @non_member_operators.each_value do |operators|
1140
+ operators.each do |op|
1141
+ cursor = op[:cursor]
1142
+ class_cursor = op[:class_cursor]
1143
+
1144
+ target_cursor, line =
1145
+ if ostream_insertion?(cursor)
1146
+ render_ostream_inspect(cursor)
1147
+ elsif cursor.type.args_size == 1
1148
+ render_unary_operator(cursor, class_cursor)
1149
+ else
1150
+ render_binary_operator(cursor, class_cursor)
1151
+ end
1152
+
1153
+ target_class = target_cursor.cruby_name
1154
+ grouped[target_class][:cpp_type] ||= @type_speller.qualified_class_name(target_cursor)
1155
+ grouped[target_class][:lines] << line
1156
+ end
1157
+ end
1158
+
1159
+ result = []
1160
+ grouped.each do |cruby_name, info|
1161
+ next if info[:lines].empty?
1162
+ # Local-defined classes use the variable; cross-file refs use Data_Type<T>()
1163
+ class_ref = @classes.key?(cruby_name) ? cruby_name : "Data_Type<#{info[:cpp_type]}>()"
1164
+ # Indent 4 spaces (2 for function body + 2 for method chain)
1165
+ content = merge_children({ nil => info[:lines] }, indentation: 4, chain: true, terminate: true, strip: true)
1166
+ result << "#{class_ref}#{content}"
1167
+ end
1168
+ result.join("\n \n ")
1169
+ end
1170
+
1171
+ # `std::ostream& operator<<(std::ostream&, T&)` overloads get translated
1172
+ # to a Ruby `inspect` method on T's class.
1173
+ def ostream_insertion?(cursor)
1174
+ return false unless cursor.spelling.include?("<<")
1175
+ arg0_decl = cursor.type.arg_type(0).non_reference_type.declaration
1176
+ arg0_decl.location.in_system_header? && arg0_decl.spelling.end_with?("ostream")
1177
+ end
1178
+
1179
+ def render_ostream_inspect(cursor)
1180
+ target_cursor = cursor.type.arg_type(1).non_reference_type.declaration
1181
+ arg_type = @type_speller.type_spelling(cursor.type.arg_type(1))
1182
+ line = render_template("non_member_operator_inspect", :arg_type => arg_type).strip
1183
+ [target_cursor, line]
1184
+ end
1185
+
1186
+ # e.g. `operator~(const Mat& m)`, `operator-(const Mat& m)`.
1187
+ def render_unary_operator(cursor, class_cursor)
1188
+ op_symbol = cursor.spelling.sub(/^operator\s*/, '')
1189
+ # Ruby uses +@ and -@ for unary plus/minus; ~ and ! stay as-is.
1190
+ ruby_name = case op_symbol
1191
+ when '+' then '+@'
1192
+ when '-' then '-@'
1193
+ else op_symbol
1194
+ end
1195
+
1196
+ line = render_template("non_member_operator_unary",
1197
+ :ruby_name => ruby_name,
1198
+ :arg0_type => @type_speller.type_spelling(cursor.type.arg_type(0)),
1199
+ :result_type => @type_speller.type_spelling(cursor.result_type),
1200
+ :op_symbol => op_symbol).strip
1201
+ [class_cursor, line]
1202
+ end
1203
+
1204
+ # e.g. `operator+(const Mat& a, const Mat& b)`.
1205
+ def render_binary_operator(cursor, class_cursor)
1206
+ op_symbol = cursor.spelling.sub(/^operator\s*/, '')
1207
+ line = render_template("non_member_operator_binary",
1208
+ :ruby_name => cursor.ruby_name,
1209
+ :arg0_type => @type_speller.type_spelling(cursor.type.arg_type(0)),
1210
+ :arg1_type => @type_speller.type_spelling(cursor.type.arg_type(1)),
1211
+ :result_type => @type_speller.type_spelling(cursor.result_type),
1212
+ :return_stmt => binary_return_stmt(cursor, op_symbol)).strip
1213
+ [class_cursor, line]
1214
+ end
1215
+
1216
+ # Pick the right return statement shape for a binary operator based on
1217
+ # what the operator returns: void, a reference to self, or a value.
1218
+ def binary_return_stmt(cursor, op_symbol)
1219
+ if cursor.result_type.kind == :type_void
1220
+ "self #{op_symbol} other;"
1221
+ elsif cursor.result_type.kind == :type_lvalue_ref &&
1222
+ cursor.result_type.non_reference_type == cursor.type.arg_type(0).non_reference_type
1223
+ # Returns reference to self (e.g., FileStorage& operator<<)
1224
+ "self #{op_symbol} other;\n return self;"
1225
+ else
1226
+ # Returns a value (e.g., bool, ptrdiff_t)
1227
+ "return self #{op_symbol} other;"
1228
+ end
1229
+ end
1230
+
1231
+ # Resolve a top-level typedef or alias to the class template specialization
1232
+ # it names, then render the specialization binding.
1233
+ def visit_typedef_decl(cursor)
1234
+ return if cursor.semantic_parent.kind == :cursor_class_decl || cursor.semantic_parent.kind == :cursor_struct
1235
+ return if skip_symbol?(cursor)
1236
+
1237
+ # Skip if already processed (can happen when force-generating base classes)
1238
+ return if @classes.key?(cursor.cruby_name)
1239
+
1240
+ # Skip typedefs to std:: types - Rice handles these automatically
1241
+ canonical_decl = cursor.underlying_type.canonical.declaration
1242
+ return if canonical_decl.kind != :cursor_no_decl_found && canonical_decl.location.in_system_header?
1243
+
1244
+ template_specialization = template_specialization_target(cursor)
1245
+ return unless template_specialization
1246
+
1247
+ cursor_template, underlying_type = template_specialization
1248
+ return if skip_symbol?(cursor_template)
1249
+
1250
+ visit_template_specialization(cursor, cursor_template, underlying_type)
1251
+ end
1252
+
1253
+ # Handle C++11 'using' type alias declarations the same as typedef
1254
+ def visit_type_alias_decl(cursor)
1255
+ visit_typedef_decl(cursor)
1256
+ end
1257
+
1258
+ # Resolve a typedef or type alias to the class template specialization it names.
1259
+ # Handles both direct specializations:
1260
+ # typedef Point_<int> Point2i;
1261
+ # and aliases through an existing typedef:
1262
+ # typedef Point2i Point;
1263
+ def template_specialization_target(cursor)
1264
+ type = cursor.underlying_type
1265
+
1266
+ loop do
1267
+ declaration = type.declaration
1268
+ return nil if declaration.kind == :cursor_no_decl_found
1269
+
1270
+ template_cursor = declaration.specialized_template
1271
+ return [template_cursor, type] unless template_cursor.kind == :cursor_invalid_file
1272
+
1273
+ return nil unless declaration.kind == :cursor_typedef_decl || declaration.kind == :cursor_type_alias_decl
1274
+
1275
+ type = declaration.underlying_type
1276
+ end
1277
+ end
1278
+
1279
+ # Render one typedef/alias specialization of a class template and ensure
1280
+ # any inherited template bases are available first.
1281
+ def visit_template_specialization(cursor, cursor_template, underlying_type)
1282
+ under = find_under(cursor)
1283
+ # Get template arguments including any default values that were omitted in the typedef
1284
+ template_argument_values = @template_resolver.full_template_arguments(cursor, underlying_type, cursor_template)
1285
+ template_arguments = template_argument_values.join(", ")
1286
+
1287
+ result = ""
1288
+
1289
+ # Is there a base class?
1290
+ base_ref = cursor_template.find_first_by_kind(false, :cursor_cxx_base_specifier)
1291
+ base_spelling = nil
1292
+ if base_ref
1293
+ # Base class children can be a :cursor_type_ref or :cursor_template_ref
1294
+ type_ref = base_ref.find_first_by_kind(false, :cursor_type_ref, :cursor_template_ref)
1295
+ base = type_ref.definition
1296
+
1297
+ # Resolve the base class to its instantiated form (e.g., PtrStep<unsigned char>)
1298
+ base_spelling = @template_resolver.resolve_base_instantiation(cursor, underlying_type)
1299
+
1300
+ # Ensure the base class is generated before this class.
1301
+ # Skip std:: base classes — Rice handles standard library types automatically.
1302
+ # This prevents walking libstdc++ internals (e.g., cv::Ptr<T> inherits from
1303
+ # std::shared_ptr<T> → std::__shared_ptr<T> → std::__shared_ptr_access<T>).
1304
+ # Also check the base template's own namespace since resolve_base_instantiation
1305
+ # may incorrectly use the derived class's namespace (e.g., "cv::shared_ptr").
1306
+ base_template_decl = template_cursor_definition(base_ref.find_first_by_kind(false, :cursor_type_ref, :cursor_template_ref)&.referenced)
1307
+ base_in_std = base_template_decl&.location&.in_system_header?
1308
+ # Use cursor_template_ref specifically to get the base class template declaration
1309
+ # (cursor_type_ref may resolve to a template parameter like T instead)
1310
+ base_class_template = template_cursor_definition(base_ref.find_first_by_kind(false, :cursor_template_ref)&.referenced)
1311
+ base_in_current_file = base_class_template &&
1312
+ translation_unit_file?(base_class_template)
1313
+ if base_spelling && !base_in_std
1314
+ if base_in_current_file
1315
+ base_typedef = @type_index.typedef_for(base_spelling)
1316
+ if base_typedef
1317
+ # Base has a typedef - check if it has been generated yet
1318
+ unless @classes.key?(base_typedef.cruby_name)
1319
+ # Force generate the typedef first (recursively handles its bases)
1320
+ result = visit_typedef_decl(base_typedef) || ""
1321
+ end
1322
+ elsif !@auto_generated_bases.include?(base_spelling)
1323
+ # No typedef - auto-generate
1324
+ result = auto_generate_base_class(base_ref, base_spelling, under)
1325
+ end
1326
+ elsif base_class_template && !base_class_template.location.in_system_header?
1327
+ # Base template is from an included file — include its .ipp so the
1328
+ # _instantiate builder is available for the derived class
1329
+ builder_ipp = ipp_path_for_cursor(base_class_template)
1330
+ current_ipp = File.join(@relative_dir, "#{@basename}.ipp")
1331
+ if builder_ipp != current_ipp
1332
+ ipp_relative = Pathname.new(builder_ipp).relative_path_from(Pathname.new(@relative_dir)).to_s
1333
+ @includes << "#include \"#{ipp_relative}\""
1334
+ end
1335
+ end
1336
+ end
1337
+ end
1338
+
1339
+ template_specialization = @template_resolver.specialization_spelling(cursor, underlying_type, cursor_template)
1340
+
1341
+ # If template is defined in a different file, include its .ipp for the _instantiate builder
1342
+ template_owner = template_cursor_definition(cursor_template)
1343
+ unless translation_unit_file?(template_owner)
1344
+ builder_ipp = ipp_path_for_cursor(template_owner)
1345
+ current_ipp = File.join(@relative_dir, "#{@basename}.ipp")
1346
+ if builder_ipp != current_ipp
1347
+ ipp_relative = Pathname.new(builder_ipp).relative_path_from(Pathname.new(@relative_dir)).to_s
1348
+ @includes << "#include \"#{ipp_relative}\""
1349
+ end
1350
+ end
1351
+
1352
+ @classes[cursor.cruby_name] = template_specialization
1353
+ result + self.render_cursor(cursor, "class_template_specialization",
1354
+ :cursor_template => cursor_template,
1355
+ :template_specialization => template_specialization,
1356
+ :template_arguments => template_arguments,
1357
+ :base_ref => base_ref,
1358
+ :base => base,
1359
+ :base_spelling => base_spelling,
1360
+ :under => under)
1361
+ end
1362
+
1363
+ # Auto-instantiate a class template used as a parameter type without a typedef.
1364
+ def auto_instantiate_template(cursor_template, instantiated_type, type, under)
1365
+ return "" unless cursor_template
1366
+ match = instantiated_type.match(/\<(.*)\>/)
1367
+ return "" unless match
1368
+
1369
+ # Skip if template arguments reference skipped symbols
1370
+ return "" if type_references_skipped_symbol?(type)
1371
+
1372
+ ruby_class_name = instantiated_type.gsub(/::|<|>|,|\s+/, ' ').split.map(&:capitalize).join
1373
+ ruby_class_name = @namer.apply_rename_types(ruby_class_name)
1374
+ cruby_name = "rb_c#{ruby_class_name}"
1375
+ return "" if @classes.key?(cruby_name)
1376
+
1377
+ @classes[cruby_name] = instantiated_type
1378
+ render_template("class_template_specialization",
1379
+ :cursor => cursor_template, :cursor_template => cursor_template,
1380
+ :template_specialization => instantiated_type, :template_arguments => match[1],
1381
+ :cruby_name => cruby_name, :base_ref => nil, :base => nil,
1382
+ :base_spelling => nil, :under => under)
1383
+ end
1384
+
1385
+ # Auto-generate a base class definition when no typedef exists for it.
1386
+ # When recursive: true, also generates any base classes of the base class.
1387
+ def auto_generate_base_class(base_ref, base_spelling, under, recursive: true)
1388
+ base_template_ref = base_ref.find_first_by_kind(false, :cursor_template_ref)
1389
+ return "" unless base_template_ref
1390
+
1391
+ base_declaration = base_ref.type.declaration
1392
+ unless base_declaration.kind == :cursor_no_decl_found
1393
+ specialized_template = base_declaration.specialized_template
1394
+ return "" if specialized_template.kind == :cursor_class_template_partial_specialization
1395
+ end
1396
+
1397
+ base_template = base_template_ref.referenced
1398
+ return "" if base_template.location.in_system_header?
1399
+ return "" if base_template.kind == :cursor_class_template_partial_specialization
1400
+ # Skip base templates from included files — their own output handles registration
1401
+ return "" unless translation_unit_file?(base_template)
1402
+ base_template_arguments_text = @template_resolver.template_argument_list_text(base_spelling)
1403
+ return "" unless base_template_arguments_text
1404
+
1405
+ base_template_arguments = @template_resolver.template_argument_texts(base_template_arguments_text)
1406
+ return "" if base_template_arguments.empty?
1407
+
1408
+ result = ""
1409
+ base_base_spelling = nil
1410
+
1411
+ # Check if this base class has its own base that needs auto-generation
1412
+ if recursive
1413
+ base_base_ref = base_template.find_first_by_kind(false, :cursor_cxx_base_specifier)
1414
+ if base_base_ref
1415
+ base_base_template_ref = base_base_ref.find_first_by_kind(false, :cursor_template_ref)
1416
+ if base_base_template_ref
1417
+ # Build substitution map from template params to actual values
1418
+ template_params = @template_resolver.template_parameters(base_template).map(&:spelling)
1419
+ template_arg_values = base_template_arguments
1420
+ subs = template_params.each_with_index.to_h { |param, i| [param, template_arg_values[i]] }
1421
+
1422
+ # Substitute template parameters with actual values
1423
+ base_base_spelling = @template_resolver.resolve_base_specifier_spelling(base_base_ref, substitutions: subs)
1424
+
1425
+ if base_base_spelling && !@type_index.typedef_for(base_base_spelling) && !@auto_generated_bases.include?(base_base_spelling)
1426
+ result = auto_generate_base_class(base_base_ref, base_base_spelling, under)
1427
+ end
1428
+ end
1429
+ end
1430
+ end
1431
+
1432
+ @auto_generated_bases << base_spelling
1433
+ ruby_name = @template_resolver.ruby_name_from_template(base_spelling, base_template_arguments)
1434
+ cruby_name = "rb_c#{ruby_name}"
1435
+
1436
+ @classes[cruby_name] = base_spelling
1437
+ result + render_template("auto_generated_base_class",
1438
+ :cruby_name => cruby_name, :ruby_name => ruby_name,
1439
+ :base_spelling => base_spelling, :base_base_spelling => base_base_spelling,
1440
+ :base_template => base_template, :template_arguments => base_template_arguments_text,
1441
+ :under => under)
1442
+ end
1443
+
1444
+ # Auto-generate a template base class for a non-template derived class.
1445
+ # For example: class PlaneWarper : public WarperBase<PlaneProjector>
1446
+ def auto_generate_template_base_for_class(base_specifier, base_spelling, under)
1447
+ auto_generate_base_class(base_specifier, base_spelling, under, recursive: false)
1448
+ end
1449
+
1450
+ # Render a union plus any embedded unions/structs that need to appear first.
1451
+ def visit_union(cursor)
1452
+ return if cursor.forward_declaration?
1453
+ return if cursor.anonymous?
1454
+ return if skip_symbol?(cursor)
1455
+
1456
+ result = Array.new
1457
+
1458
+ # Define any embedded unions (skip anonymous/skipped ones that return nil)
1459
+ cursor.find_by_kind(false, :cursor_union) do |union|
1460
+ content = visit_union(union)
1461
+ result << content if content
1462
+ end
1463
+
1464
+ # Define any embedded structures (skip anonymous/skipped ones that return nil)
1465
+ cursor.find_by_kind(false, :cursor_struct) do |struct|
1466
+ content = visit_struct(struct)
1467
+ result << content if content
1468
+ end
1469
+
1470
+ under = find_under(cursor)
1471
+
1472
+ children = render_children(cursor, indentation: 2, chain: true, terminate: true, strip: true,
1473
+ exclude_kinds: Set[:cursor_struct, :cursor_union])
1474
+ result << self.render_cursor(cursor, "union", :under => under, :children => children,
1475
+ :cpp_type => @type_speller.qualified_class_name(cursor),
1476
+ :ruby_name => cursor.ruby_name)
1477
+ result.map { |s| s.chomp }.join("\n\n")
1478
+ end
1479
+
1480
+ # Render a variable either as a Ruby constant or, for static class members,
1481
+ # as a singleton attribute on the owning Rice class.
1482
+ def visit_variable(cursor)
1483
+ if CURSOR_CLASSES.include?(cursor.semantic_parent.kind) &&
1484
+ !cursor.public?
1485
+ return
1486
+ end
1487
+ return if skip_symbol?(cursor)
1488
+
1489
+ # Skip compiler/cuda keywords like __device__ __forceinline__
1490
+ return if cursor.spelling.match(/^__.*__$/)
1491
+
1492
+ # Const variables become Ruby constants
1493
+ if cursor.type.const_qualified?
1494
+ visit_variable_constant(cursor)
1495
+ else
1496
+ parent_kind = cursor.semantic_parent.kind
1497
+ if parent_kind == :cursor_translation_unit || parent_kind == :cursor_namespace
1498
+ # Non-const variables at global/namespace scope become Ruby constants
1499
+ # Rice's define_singleton_attr only works on Data_Type<T>, not Class or Module
1500
+ visit_variable_constant(cursor)
1501
+ else
1502
+ # Static class fields use define_singleton_attr on Data_Type<T>
1503
+ qualified_parent = @type_speller.qualified_display_name(cursor.semantic_parent)
1504
+ self.render_cursor(cursor, "variable",
1505
+ :qualified_parent => qualified_parent)
1506
+ end
1507
+ end
1508
+ end
1509
+
1510
+ # Render one constant definition for an enum value, macro, or variable.
1511
+ def visit_variable_constant(cursor)
1512
+ self.render_cursor(cursor, "constant",
1513
+ :name => cursor.spelling.upcase_first,
1514
+ :qualified_name => @type_speller.qualified_display_name(cursor))
1515
+ end
1516
+
1517
+ # Render the optional project-level wrapper files that call every generated
1518
+ # per-header init function.
1519
+ def create_project_files
1520
+ return unless @project
1521
+
1522
+ # Create master hpp/cpp files to include all the files we generated
1523
+ basename = "#{project}-rb"
1524
+ rice_header = "#{basename}.hpp"
1525
+ rice_cpp = "#{basename}.cpp"
1526
+ init_function = "Init_#{project}"
1527
+
1528
+ content = render_template("project.hpp",
1529
+ :init_name => init_function, :init_names => @init_names)
1530
+ self.outputter.write(rice_header, content)
1531
+
1532
+ content = render_template("project.cpp",
1533
+ :project_header => rice_header, :init_name => init_function, :init_names => @init_names)
1534
+ self.outputter.write(rice_cpp, content)
1535
+ end
1536
+
1537
+ # Map a cursor kind such as `:cursor_class_decl` to the corresponding
1538
+ # visitor method symbol, for example `:visit_class_decl`.
1539
+ def figure_method(cursor)
1540
+ name = cursor.kind.to_s.delete_prefix("cursor_")
1541
+ "visit_#{name.underscore}".to_sym
1542
+ end
1543
+
1544
+ # Add left padding to non-blank lines while preserving existing blank lines.
1545
+ def add_indentation(content, indentation)
1546
+ content.lines.map do |line|
1547
+ # Don't add indentation to blank lines
1548
+ line.strip.empty? ? line : " " * indentation + line
1549
+ end.join
1550
+ end
1551
+
1552
+ # Render an ERB template with the current cursor injected into the locals.
1553
+ def render_cursor(cursor, template, local_variables = {})
1554
+ render_template(template, local_variables.merge(:cursor => cursor))
1555
+ end
1556
+
1557
+ def template_cursor_definition(cursor)
1558
+ return nil unless cursor
1559
+
1560
+ definition = cursor.definition
1561
+ return cursor if definition.kind == :cursor_invalid_file || definition.kind == :cursor_no_decl_found
1562
+
1563
+ definition
1564
+ end
1565
+
1566
+ def nested_template_builder_requires_outer_context?(cursor)
1567
+ parent = cursor.semantic_parent
1568
+ while parent
1569
+ break if [:cursor_invalid_file, :cursor_no_decl_found, :cursor_translation_unit].include?(parent.kind)
1570
+ return true if [:cursor_class_template,
1571
+ :cursor_class_template_partial_specialization,
1572
+ :cursor_function_template].include?(parent.kind)
1573
+ parent = parent.semantic_parent
1574
+ end
1575
+
1576
+ false
1577
+ end
1578
+
1579
+ # Returns [content, has_builders] where has_builders indicates if any builder templates were generated
1580
+ def render_class_templates(cursor, indentation: 0, strip: false)
1581
+ results = Array.new
1582
+ cursor.find_by_kind(true, :cursor_class_template) do |class_template_cursor|
1583
+ if class_template_cursor.private? || class_template_cursor.protected?
1584
+ next :continue
1585
+ end
1586
+
1587
+ # Nested class templates inside class templates need the outer
1588
+ # template parameters and dependent parent qualification to form a
1589
+ # valid builder signature. Until that context is threaded through the
1590
+ # builder templates, skip emitting them rather than generating
1591
+ # invalid C++ such as Allocator::rebind<U>.
1592
+ if nested_template_builder_requires_outer_context?(class_template_cursor)
1593
+ next :continue
1594
+ end
1595
+
1596
+ # Skip forward declarations
1597
+ if class_template_cursor.declaration? && !class_template_cursor.definition?
1598
+ next :continue
1599
+ end
1600
+ if class_template_cursor.location.in_system_header?
1601
+ next :continue
1602
+ end
1603
+
1604
+ # Check if class template is from the main file.
1605
+ # Note: from_main_file? doesn't work when -include is used, so manually check.
1606
+ unless translation_unit_file?(class_template_cursor)
1607
+ next :continue
1608
+ end
1609
+
1610
+ # Skip if explicitly listed in symbols
1611
+ if skip_symbol?(class_template_cursor)
1612
+ next :continue
1613
+ end
1614
+
1615
+ builder = visit_class_template_builder(class_template_cursor)
1616
+ results << builder if builder
1617
+ end
1618
+ content = merge_children({ nil => results }, indentation: indentation, strip: strip)
1619
+ has_builders = !results.empty? && !content.strip.empty?
1620
+ [content, has_builders]
1621
+ end
1622
+
1623
+ # Visit eligible child cursors and bucket their rendered output by version
1624
+ # guard so later merging can emit `#if VERSION >= ...` blocks cleanly.
1625
+ def visit_children(cursor, exclude_kinds: Set.new, only_kinds: nil)
1626
+ versions = Hash.new { |h, k| h[k] = [] }
1627
+ cursor.each(false) do |child_cursor, parent_cursor|
1628
+ if child_cursor.location.in_system_header?
1629
+ next :continue
1630
+ end
1631
+
1632
+ # This sometimes does not work - for example OpenCV defines the macros
1633
+ # CV__DNN_INLINE_NS_BEGIN/CV__DNN_INLINE_NS_END in a separate header file
1634
+ # which causes from_main_file? to be false. So manually check.
1635
+ # unless child_cursor.location.from_main_file?
1636
+ unless translation_unit_file?(child_cursor)
1637
+ next :continue
1638
+ end
1639
+
1640
+ # For some reason child.cursor.public? filters out way too much
1641
+ if child_cursor.private? || child_cursor.protected?
1642
+ next :continue
1643
+ end
1644
+
1645
+ if child_cursor.deleted?
1646
+ next :continue
1647
+ end
1648
+
1649
+ child_kind = child_cursor.kind
1650
+
1651
+ unless child_cursor.declaration? || child_kind == :cursor_macro_definition
1652
+ next :continue
1653
+ end
1654
+
1655
+ if child_cursor.forward_declaration?
1656
+ next :continue
1657
+ end
1658
+
1659
+ if exclude_kinds.include?(child_kind)
1660
+ next :continue
1661
+ end
1662
+
1663
+ if only_kinds && !only_kinds.include?(child_kind)
1664
+ next :continue
1665
+ end
1666
+
1667
+ visit_method = "visit_#{child_kind.to_s.delete_prefix("cursor_").underscore}".to_sym
1668
+ if self.respond_to?(visit_method)
1669
+ content = self.send(visit_method, child_cursor)
1670
+ version = @symbols.version(child_cursor)
1671
+ case content
1672
+ when Array
1673
+ versions[version] += content
1674
+ when String
1675
+ versions[version] << content
1676
+ end
1677
+ end
1678
+ next :continue
1679
+ end
1680
+ versions
1681
+ end
1682
+
1683
+ # Merge previously rendered child content into final output text, with
1684
+ # optional method chaining, termination, indentation, and version guards.
1685
+ def merge_children(versions, indentation: 0, chain: false, terminate: false, strip: false)
1686
+ lines = versions.keys.sort_by { |key| key.to_s }.each_with_object([]) do |version, result|
1687
+ next unless versions[version]&.any?
1688
+ result << "#if #{@version_check} >= #{version}" if version
1689
+ versions[version].each do |line|
1690
+ line = line.rstrip if strip
1691
+ line = ".#{line}" if chain
1692
+ result << line
1693
+ end
1694
+ result << "#endif\n" if version
1695
+ end
1696
+
1697
+ if lines.empty?
1698
+ return terminate ? ";" : ""
1699
+ end
1700
+
1701
+ result = if chain
1702
+ lines.join("\n")
1703
+ else
1704
+ lines.map { |l| l.chomp }.reject(&:empty?).join("\n\n")
1705
+ end
1706
+ if terminate
1707
+ result += ";"
1708
+ end
1709
+
1710
+ result = add_indentation(result, indentation) if indentation > 0
1711
+ result = "\n" + result if chain || terminate
1712
+ result
1713
+ end
1714
+
1715
+ # Convenience wrapper around `visit_children` and `merge_children`.
1716
+ def render_children(cursor, indentation: 0, chain: false, terminate: false, strip: false,
1717
+ exclude_kinds: Set.new, only_kinds: nil)
1718
+ versions = visit_children(cursor, exclude_kinds: exclude_kinds, only_kinds: only_kinds)
1719
+ merge_children(versions, indentation: indentation, chain: chain, terminate: terminate, strip: strip)
1720
+ end
1721
+
1722
+ end
1723
+ end
1724
+ end