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,687 @@
1
+ module RubyBindgen
2
+ module Generators
3
+ class FFI < Generator
4
+ attr_reader :library_names, :library_versions
5
+
6
+ def self.template_dir
7
+ __dir__
8
+ end
9
+
10
+ def initialize(inputter, outputter, config)
11
+ super(inputter, outputter, config)
12
+ raise ArgumentError, "FFI format requires the 'project' option" unless @project
13
+ @version_check = config[:version_check]
14
+ @library_names = config[:library_names] || []
15
+ @library_versions = config[:library_versions] || []
16
+ @symbols = RubyBindgen::Symbols.new(config[:symbols] || {})
17
+ raise ArgumentError, "version_check is required when symbols.versions is non-empty" if @symbols.has_versions? && !@version_check
18
+ @library_search_path = config[:library_search_path]
19
+ @export_macros = config[:export_macros] || []
20
+ @module_name = config[:module]
21
+ end
22
+
23
+ def generate
24
+ clang_args = @config[:clang_args] || []
25
+ parser = RubyBindgen::Parser.new(@inputter, clang_args, libclang: @config[:libclang])
26
+ symbols_config = @config[:symbols] || {}
27
+ rename_types = RubyBindgen::NameMapper.from_config(symbols_config[:rename_types] || [])
28
+ rename_methods = RubyBindgen::NameMapper.from_config(symbols_config[:rename_methods] || [])
29
+ @namer = RubyBindgen::Namer.new(rename_types, rename_methods)
30
+ ::FFI::Clang::Cursor.namer = @namer
31
+ parser.generate(self)
32
+ end
33
+
34
+ # Check if cursor has one of the required export macros in its source text.
35
+ # When export_macros is empty, all symbols pass (no filtering).
36
+ def has_export_macro?(cursor)
37
+ return true if @export_macros.empty?
38
+
39
+ source_text = cursor.extent.text
40
+ return false if source_text.nil?
41
+ @export_macros.any? { |macro| source_text.include?(macro) }
42
+ end
43
+
44
+ def visit_start
45
+ @generated_files = []
46
+ end
47
+
48
+ def visit_translation_unit(translation_unit, path, relative_path)
49
+ basename = File.basename(relative_path, ".*").underscore
50
+ relative_path_2 = File.join(File.dirname(relative_path), "#{basename}.rb")
51
+
52
+ cursor = translation_unit.cursor
53
+ module_name = @module_name || cursor.ruby_name
54
+ module_parts = module_name.split("::")
55
+
56
+ content = render_children(cursor, indentation: module_parts.length * 2)
57
+
58
+ result = render_template("translation_unit",
59
+ :module_parts => module_parts,
60
+ :content => content.rstrip)
61
+
62
+ result.gsub!(/\n\n\n/, "\n\n")
63
+ @generated_files << File.join(File.dirname(relative_path), basename)
64
+ self.outputter.write(relative_path_2, result)
65
+ end
66
+
67
+ def visit_end
68
+ create_project_file
69
+ end
70
+
71
+ def create_project_file
72
+ return if @generated_files.empty?
73
+
74
+ module_name = @module_name || @project.camelize
75
+ module_parts = module_name.split("::")
76
+ depth = module_parts.length
77
+ library = add_indentation(render_template("library"), depth * 2)
78
+
79
+ has_versions = @symbols.has_versions?
80
+ version_file = has_versions ? "#{@project}_version" : nil
81
+
82
+ content = render_template("project",
83
+ :module_parts => module_parts,
84
+ :library => library.rstrip,
85
+ :version_file => version_file,
86
+ :files => @generated_files)
87
+
88
+ self.outputter.write("#{@project}_ffi.rb", content)
89
+
90
+ create_version_file(module_parts) if version_file
91
+ end
92
+
93
+ def create_version_file(module_parts)
94
+ relative_path = "#{@project}_version.rb"
95
+ full_path = self.outputter.output_path(relative_path)
96
+ return if File.exist?(full_path)
97
+
98
+ method_name = @version_check
99
+ depth = module_parts.length
100
+ method_body = add_indentation(render_template("version_method", :method_name => method_name), depth * 2)
101
+
102
+ content = render_template("version",
103
+ :module_parts => module_parts,
104
+ :method_body => method_body.rstrip)
105
+ self.outputter.write(relative_path, content)
106
+ end
107
+
108
+ def visit_children(cursor, exclude_kinds: Set.new)
109
+ versions = Hash.new { |h, k| h[k] = [] }
110
+ cursor.each(false) do |child_cursor, parent_cursor|
111
+ if child_cursor.location.in_system_header?
112
+ next :continue
113
+ end
114
+
115
+ # Note: from_main_file? doesn't work when -include is used, so manually check.
116
+ unless translation_unit_file?(child_cursor)
117
+ next :continue
118
+ end
119
+
120
+ # For some reason child.cursor.public? filters out way too much
121
+ if child_cursor.private? || child_cursor.protected?
122
+ next :continue
123
+ end
124
+
125
+ if child_cursor.deleted?
126
+ next :continue
127
+ end
128
+
129
+ unless child_cursor.declaration? || child_cursor.kind == :cursor_linkage_spec
130
+ next :continue
131
+ end
132
+
133
+ if child_cursor.forward_declaration?
134
+ next :continue
135
+ end
136
+
137
+ if exclude_kinds.include?(child_cursor.kind)
138
+ next :continue
139
+ end
140
+
141
+ visit_method = self.figure_method(child_cursor)
142
+ if self.respond_to?(visit_method)
143
+ content = self.send(visit_method, child_cursor)
144
+ version = @symbols.version(child_cursor)
145
+ case content
146
+ when Array
147
+ versions[version] += content
148
+ when String
149
+ versions[version] << content
150
+ end
151
+ end
152
+ next :continue
153
+ end
154
+ versions
155
+ end
156
+
157
+ def merge_children(versions, indentation: 0, comma: false, strip: false)
158
+ lines = versions.keys.sort_by { |key| key.to_s }.each_with_object([]) do |version, result|
159
+ next unless versions[version]&.any?
160
+ result << "if #{@version_check} >= #{version}" if version
161
+ versions[version].each do |line|
162
+ line = line.rstrip if strip
163
+ line = add_indentation(line, 2) if version
164
+ result << line
165
+ end
166
+ result << "end" if version
167
+ end
168
+
169
+ return "" if lines.empty?
170
+
171
+ separator = comma ? ",\n" : "\n"
172
+ result = lines.join(separator)
173
+ result = add_indentation(result, indentation) if indentation > 0
174
+ result
175
+ end
176
+
177
+ def render_children(cursor, indentation: 0, comma: false, strip: false, exclude_kinds: Set.new)
178
+ versions = visit_children(cursor, exclude_kinds: exclude_kinds)
179
+ merge_children(versions, indentation: indentation, comma: comma, strip: strip)
180
+ end
181
+
182
+ def visit_callback(name, parameters, type)
183
+ parameter_types = parameters.map do |parameter|
184
+ if parameter.find_first_by_kind(false, :cursor_type_ref) && parameter.type.is_a?(::FFI::Clang::Types::Pointer)
185
+ ":pointer"
186
+ else
187
+ figure_ffi_type(parameter.type, :callback)
188
+ end
189
+ end
190
+ self.render_callback(name, parameter_types, type.result_type)
191
+ end
192
+
193
+ def visit_enum_decl(cursor)
194
+ return if @symbols.skip?(cursor)
195
+
196
+ versions = visit_children(cursor)
197
+ has_versioned_constants = versions.keys.any? { |k| !k.nil? }
198
+
199
+ if has_versioned_constants
200
+ render_versioned_enum(cursor, versions)
201
+ else
202
+ children = merge_children(versions, indentation: 2, comma: true, strip: true)
203
+ template = cursor.anonymous? ? "enum_decl_anonymous" : "enum_decl"
204
+ self.render_cursor(cursor, template, :children => children)
205
+ end
206
+ end
207
+
208
+ def visit_enum_constant_decl(cursor)
209
+ self.render_cursor(cursor,"enum_constant_decl")
210
+ end
211
+
212
+ # extern "C" {} — transparent wrapper, recurse into children
213
+ def visit_linkage_spec(cursor)
214
+ versions = visit_children(cursor)
215
+ merge_children(versions)
216
+ end
217
+
218
+ def visit_function(cursor)
219
+ return if cursor.availability == :deprecated
220
+ return if @symbols.skip?(cursor)
221
+ return if references_skipped_type?(cursor.type.result_type)
222
+ return if has_skipped_param_type?(cursor)
223
+ return if has_va_list_param?(cursor)
224
+ return unless has_export_macro?(cursor)
225
+
226
+ signature = @symbols.override(cursor)
227
+ if signature
228
+ return self.render_cursor(cursor, "function", :parameter_types => nil, :signature => signature)
229
+ end
230
+
231
+ result = Array.new
232
+ parameter_types = cursor.find_by_kind(false, :cursor_parm_decl).map do |parameter|
233
+ callback_name = "#{cursor.spelling.underscore}_#{parameter.spelling.underscore}_callback"
234
+ if parameter.type.is_a?(::FFI::Clang::Types::Pointer) && parameter.type.function?
235
+ parameters = parameter.find_by_kind(false, :cursor_parm_decl)
236
+ result << self.visit_callback(callback_name, parameters, parameter.type.pointee)
237
+ end
238
+
239
+ if parameter.type.is_a?(::FFI::Clang::Types::Pointer) && parameter.type.function?
240
+ ":#{callback_name}"
241
+ else
242
+ figure_ffi_type(parameter.type, :function)
243
+ end
244
+ end
245
+ parameter_types << ":varargs" if cursor.type.variadic?
246
+ result << self.render_cursor(cursor, "function", :parameter_types => parameter_types, :signature => nil)
247
+ result.join("\n")
248
+ end
249
+
250
+ def visit_struct(cursor)
251
+ return if cursor.forward_declaration?
252
+ return if cursor.opaque_declaration?
253
+ return if @symbols.skip?(cursor)
254
+ return if cursor.anonymous? && !cursor.anonymous_definer
255
+
256
+ result = Hash.new { |h, k| h[k] = [] }
257
+
258
+ # Define any embedded structures
259
+ cursor.find_by_kind(false, :cursor_struct).each do |struct|
260
+ content = visit_struct(struct)
261
+ next unless content
262
+ version = @symbols.version(struct)
263
+ result[version] << content
264
+ end
265
+
266
+ # Define any embedded unions
267
+ cursor.find_by_kind(false, :cursor_union).each do |union|
268
+ content = visit_union(union)
269
+ next unless content
270
+ version = @symbols.version(union)
271
+ result[version] << content
272
+ end
273
+
274
+ # Define any embedded callbacks
275
+ cursor.find_by_kind(false, :cursor_field_decl).each do |field|
276
+ if field.type.is_a?(::FFI::Clang::Types::Pointer) && field.type.function?
277
+ callback_name = "#{cursor.spelling.underscore}_#{field.spelling.underscore}_callback"
278
+ parameters = field.find_by_kind(false, :cursor_parm_decl)
279
+ result[nil] << self.visit_callback(callback_name, parameters, field.type.pointee)
280
+ end
281
+ end
282
+
283
+ versions = visit_children(cursor, exclude_kinds: [:cursor_struct, :cursor_union])
284
+ has_versioned_fields = versions.keys.any? { |k| !k.nil? }
285
+
286
+ if has_versioned_fields
287
+ result[nil] << render_versioned_layout(cursor, versions, "struct")
288
+ else
289
+ children = merge_children(versions, indentation: 9, comma: true, strip: true)
290
+ result[nil] << self.render_cursor(cursor, "struct", :children => children.lstrip)
291
+ end
292
+ merge_children(result)
293
+ end
294
+
295
+ def visit_field_decl(cursor)
296
+ ffi_type = if cursor.type.is_a?(::FFI::Clang::Types::Pointer)
297
+ if cursor.type.function?
298
+ ":#{cursor.semantic_parent.spelling.underscore}_#{cursor.spelling.underscore}_callback"
299
+ elsif pointer_to_forward_declaration?(cursor.type)
300
+ ":pointer"
301
+ end
302
+ end
303
+
304
+ ffi_type ||= figure_ffi_type(cursor.type, :structure)
305
+ self.render_cursor(cursor, "field_decl", ffi_type: ffi_type)
306
+ end
307
+
308
+ def visit_typedef_decl(cursor)
309
+ return if @symbols.skip?(cursor)
310
+
311
+ underlying_type = cursor.underlying_type
312
+ canonical_type = underlying_type.canonical
313
+
314
+ if [:type_record, :type_enum].include?(canonical_type.kind)
315
+ # Opaque struct/union typedef - emit typedef :pointer
316
+ if canonical_type.declaration.opaque_declaration?
317
+ return render_cursor(cursor, "typedef_decl")
318
+ end
319
+ # Otherwise it's a struct/union/enum we have already rendered - skip
320
+ return
321
+ elsif underlying_type.kind == :type_pointer && underlying_type.function?
322
+ func_type = underlying_type.pointee
323
+ parameter_types = (0...func_type.args_size).map { |i| figure_ffi_type(func_type.arg_type(i), :callback) }
324
+ return render_callback(cursor.ruby_name, parameter_types, func_type.result_type)
325
+ end
326
+ render_cursor(cursor, "typedef_decl")
327
+ end
328
+
329
+ def visit_union(cursor)
330
+ return if cursor.forward_declaration?
331
+ return if cursor.opaque_declaration?
332
+ return if @symbols.skip?(cursor)
333
+ return if cursor.anonymous? && !cursor.anonymous_definer
334
+
335
+ result = Hash.new { |h, k| h[k] = [] }
336
+
337
+ # Define any embedded unions
338
+ cursor.find_by_kind(false, :cursor_union).each do |union|
339
+ content = visit_union(union)
340
+ next unless content
341
+ version = @symbols.version(union)
342
+ result[version] << content
343
+ end
344
+
345
+ # Define any embedded structures
346
+ cursor.find_by_kind(false, :cursor_struct).each do |struct|
347
+ content = visit_struct(struct)
348
+ next unless content
349
+ version = @symbols.version(struct)
350
+ result[version] << content
351
+ end
352
+
353
+ # Define any embedded callbacks
354
+ cursor.find_by_kind(false, :cursor_field_decl).each do |field|
355
+ if field.type.is_a?(::FFI::Clang::Types::Pointer) && field.type.function?
356
+ callback_name = "#{cursor.spelling.underscore}_#{field.spelling.underscore}_callback"
357
+ parameters = field.find_by_kind(false, :cursor_parm_decl)
358
+ result[nil] << self.visit_callback(callback_name, parameters, field.type.pointee)
359
+ end
360
+ end
361
+
362
+ versions = visit_children(cursor, exclude_kinds: [:cursor_struct, :cursor_union])
363
+ has_versioned_fields = versions.keys.any? { |k| !k.nil? }
364
+
365
+ if has_versioned_fields
366
+ result[nil] << render_versioned_layout(cursor, versions, "union")
367
+ else
368
+ children = merge_children(versions, indentation: 9, comma: true, strip: true)
369
+ result[nil] << self.render_cursor(cursor, "union", :children => children.lstrip)
370
+ end
371
+ merge_children(result)
372
+ end
373
+
374
+ def visit_variable(cursor)
375
+ return if @symbols.skip?(cursor)
376
+
377
+ if cursor.type.const_qualified?
378
+ tokens = cursor.translation_unit.tokenize(cursor.extent)
379
+ eq_index = tokens.tokens.index { |t| t.spelling == "=" }
380
+ if eq_index && tokens.tokens[eq_index + 1]&.kind == :literal
381
+ return render_cursor(cursor, "constant",
382
+ name: cursor.ruby_name,
383
+ value: literal_to_ruby(tokens.tokens[eq_index + 1].spelling))
384
+ end
385
+ end
386
+
387
+ self.render_cursor(cursor, "variable")
388
+ end
389
+
390
+ # Convert a C literal token spelling into something that parses as Ruby.
391
+ # libclang gives us the verbatim source text, so C numeric suffixes
392
+ # (2.5f, 100000L, 42U, 1ULL, 0xABCDuLL) flow through unchanged and break
393
+ # the generated module. Char ('A') and string ("hello") literals are
394
+ # returned as-is — they're already valid Ruby strings.
395
+ #
396
+ # Hex literals need different treatment: f/F are valid hex digits, so
397
+ # 0xff must NOT have its trailing f stripped (would yield 0xf = 15
398
+ # instead of 255). Hex strips integer suffixes only.
399
+ C_INT_SUFFIX = /(?:[uU][lL]{0,2}|[lL]{1,2}[uU]?)\z/.freeze
400
+ C_FLOAT_SUFFIX = /(?:[uU][lL]{0,2}|[lL]{1,2}[uU]?|[fFlL])\z/.freeze
401
+ def literal_to_ruby(spelling)
402
+ return spelling if spelling.start_with?("'", '"')
403
+ suffix = spelling.match?(/\A[+-]?0[xX]/) ? C_INT_SUFFIX : C_FLOAT_SUFFIX
404
+ spelling.sub(suffix, '')
405
+ end
406
+
407
+ def figure_method(cursor)
408
+ name = cursor.kind.to_s.delete_prefix("cursor_")
409
+ "visit_#{name.underscore}".to_sym
410
+ end
411
+
412
+ # Check if any parameter is a va_list type, which cannot be constructed from Ruby.
413
+ # va_list representation varies by platform:
414
+ # Linux x86_64: elaborated(va_list) -> typedef(__builtin_va_list) -> struct __va_list_tag[1]
415
+ # Other targets: may use __builtin_va_list, __gnuc_va_list, or other forms
416
+ # The typedef declaration spelling is the most reliable check.
417
+ VA_LIST_NAMES = Set.new(%w[va_list __builtin_va_list __gnuc_va_list]).freeze
418
+
419
+ def has_va_list_param?(cursor)
420
+ cursor.find_by_kind(false, :cursor_parm_decl).any? do |param|
421
+ type = param.type
422
+ # Check declaration spelling (works for elaborated and typedef types)
423
+ decl = type.declaration
424
+ next true if decl.kind == :cursor_typedef_decl && VA_LIST_NAMES.include?(decl.spelling)
425
+
426
+ # Fallback: check canonical type for __va_list_tag (Linux x86_64 array form)
427
+ canonical = type.canonical
428
+ canonical.kind == :type_constant_array &&
429
+ canonical.element_type.kind == :type_record &&
430
+ canonical.element_type.declaration.spelling == "__va_list_tag"
431
+ end
432
+ end
433
+
434
+ # Check if any parameter type of a function references a skipped symbol.
435
+ def has_skipped_param_type?(cursor)
436
+ (0...cursor.type.args_size).any? do |i|
437
+ references_skipped_type?(cursor.type.arg_type(i))
438
+ end
439
+ end
440
+
441
+ # Check if a type references a skipped symbol (unwrapping pointers).
442
+ def references_skipped_type?(type)
443
+ type = type.intrinsic_type
444
+ decl = type.declaration
445
+ return false if decl.kind == :cursor_no_decl_found
446
+ @symbols.skip?(decl)
447
+ end
448
+
449
+ def pointer_to_forward_declaration?(type)
450
+ return false unless type.kind == :type_pointer
451
+
452
+ decl = type.pointee.canonical.declaration
453
+ return false if [:cursor_invalid_file, :cursor_no_decl_found].include?(decl.kind)
454
+
455
+ decl.opaque_declaration?
456
+ end
457
+
458
+ def figure_ffi_type(type, context = nil)
459
+ case type.kind
460
+ when :type_bool
461
+ ":bool"
462
+ when :type_float
463
+ ":float"
464
+ when :type_double
465
+ ":double"
466
+ when :type_int
467
+ ":int"
468
+ when :type_long
469
+ ":long"
470
+ when :type_longlong
471
+ ":long_long"
472
+ when :type_ulong
473
+ ":ulong"
474
+ when :type_ulonglong
475
+ ":ulong_long"
476
+ when :type_uint
477
+ ":uint"
478
+ when :type_short
479
+ ":short"
480
+ when :type_ushort
481
+ ":ushort"
482
+ when :type_char_s
483
+ ":char"
484
+ when :type_uchar, :type_char_u
485
+ ":uchar"
486
+ when :type_schar
487
+ ":int8"
488
+ when :type_wchar
489
+ figure_ffi_type(type.canonical, context)
490
+ when :type_char16
491
+ ":uint16"
492
+ when :type_char32
493
+ ":uint32"
494
+ when :type_longdouble
495
+ ":long_double"
496
+ when :type_int128, :type_uint128
497
+ raise("Unsupported 128-bit integer type: #{type.kind}")
498
+ when :type_void
499
+ ":void"
500
+ when :type_elaborated
501
+ figure_ffi_declared_type(type, context)
502
+ when :type_record
503
+ figure_ffi_record_type(type, context)
504
+ when :type_typedef
505
+ figure_ffi_declared_type(type, context)
506
+ when :type_pointer
507
+ figure_ffi_pointer_type(type, context)
508
+ when :type_enum
509
+ type.declaration.ruby_name
510
+ when :type_constant_array
511
+ case context
512
+ when :structure
513
+ "[#{figure_ffi_type(type.element_type)}, #{type.size}]"
514
+ else
515
+ ":pointer"
516
+ end
517
+ when :type_incomplete_array
518
+ case type.element_type.kind
519
+ when :type_char_s
520
+ ":string"
521
+ else
522
+ raise("Unsupported incomplete array type: #{type.element_type.kind}")
523
+ end
524
+ when :type_block_pointer
525
+ # Objective-C block pointers (macOS) — treat as opaque pointer
526
+ ":pointer"
527
+ else
528
+ raise("Unsupported type: #{type.kind}")
529
+ end
530
+ end
531
+
532
+ def figure_ffi_declared_type(type, context = nil)
533
+ if type.declaration.spelling == "va_list"
534
+ # va_list cannot be constructed from Ruby — functions with va_list
535
+ # params are skipped in visit_function. Map to :pointer as fallback.
536
+ ":pointer"
537
+ elsif type.canonical.kind == :type_pointer && type.canonical.function?
538
+ # Typedef'd function pointer (callback) — use the callback name
539
+ ":#{type.declaration.ruby_name}"
540
+ elsif type.canonical.kind == :type_function_proto
541
+ ":pointer"
542
+ elsif type.canonical.kind == :type_record
543
+ figure_ffi_record_type(type, context)
544
+ elsif type.canonical.kind == :type_enum
545
+ figure_ffi_type(type.canonical, context)
546
+ else
547
+ spelling = type.declaration.spelling
548
+ if ::FFI::TypeDefs.key?(spelling.to_sym)
549
+ ":#{spelling}"
550
+ else
551
+ self.figure_ffi_type(type.canonical, context)
552
+ end
553
+ end
554
+ end
555
+
556
+ def figure_ffi_record_type(type, context = nil)
557
+ if type.canonical.declaration.opaque_declaration?
558
+ return ":pointer"
559
+ end
560
+ if type.anonymous?
561
+ definer = type.declaration.anonymous_definer
562
+ return definer ? definer.spelling.camelize : ":pointer"
563
+ end
564
+
565
+ case
566
+ when context == :function
567
+ "#{type.declaration.ruby_name}.by_value"
568
+ when context == :callback
569
+ "#{type.declaration.ruby_name}.by_value"
570
+ else
571
+ type.declaration.ruby_name
572
+ end
573
+ end
574
+
575
+ def figure_ffi_pointer_type(type, context)
576
+ case type.pointee.kind
577
+ when :type_char_s
578
+ case context
579
+ when :callback_return
580
+ ":pointer"
581
+ else
582
+ type.pointee.const_qualified? ? ":string" : ":pointer"
583
+ end
584
+ else
585
+ figure_ffi_record_pointer_type(type.pointee, context) || ":pointer"
586
+ end
587
+ end
588
+
589
+ def figure_ffi_record_pointer_type(type, context)
590
+ return nil unless type.canonical.kind == :type_record
591
+ return ":pointer" if type.canonical.declaration.opaque_declaration?
592
+
593
+ case context
594
+ when :union, :structure, :typedef
595
+ "#{type.canonical.declaration.ruby_name}.ptr"
596
+ when :function, :callback, :callback_return
597
+ "#{type.canonical.declaration.ruby_name}.by_ref"
598
+ else
599
+ type.canonical.declaration.ruby_name
600
+ end
601
+ end
602
+
603
+
604
+ def add_indentation(content, indentation)
605
+ content.lines.map do |line|
606
+ if line.strip.empty?
607
+ line
608
+ else
609
+ " " * indentation + line
610
+ end
611
+ end.join
612
+ end
613
+
614
+ # Render a struct/union with versioned fields as separate definitions per version threshold.
615
+ # Each version gets a complete definition with cumulative fields up to that version.
616
+ # Output is wrapped in if/elsif/else guards.
617
+ def render_versioned_layout(cursor, versions, template)
618
+ # Build sorted version thresholds (nil = unversioned, always included)
619
+ thresholds = versions.keys.compact.sort.reverse
620
+
621
+ # Build cumulative field lists: each threshold includes all fields from lower versions
622
+ lines = []
623
+ first = true
624
+ thresholds.each do |threshold|
625
+ guard = first ? "if" : "elsif"
626
+ first = false
627
+ lines << "#{guard} #{@version_check} >= #{threshold}"
628
+
629
+ # Collect all fields: unversioned + all versions <= threshold
630
+ cumulative_fields = (versions[nil] || []).dup
631
+ versions.keys.compact.sort.each do |v|
632
+ cumulative_fields.concat(versions[v]) if v <= threshold
633
+ end
634
+
635
+ children = cumulative_fields.map(&:rstrip).join(",\n" + " " * 9)
636
+ lines << add_indentation(render_cursor(cursor, template, :children => children).strip, 2)
637
+ end
638
+
639
+ # else branch: unversioned fields only (may be empty, but type must still be defined)
640
+ base_fields = versions[nil] || []
641
+ lines << "else"
642
+ children = base_fields.map(&:rstrip).join(",\n" + " " * 9)
643
+ lines << add_indentation(render_cursor(cursor, template, :children => children).strip, 2)
644
+ lines << "end"
645
+ lines.join("\n")
646
+ end
647
+
648
+ # Render an enum with versioned constants as separate definitions per version threshold.
649
+ def render_versioned_enum(cursor, versions)
650
+ thresholds = versions.keys.compact.sort.reverse
651
+
652
+ lines = []
653
+ first = true
654
+ thresholds.each do |threshold|
655
+ guard = first ? "if" : "elsif"
656
+ first = false
657
+ lines << "#{guard} #{@version_check} >= #{threshold}"
658
+
659
+ cumulative = (versions[nil] || []).dup
660
+ versions.keys.compact.sort.each do |v|
661
+ cumulative.concat(versions[v]) if v <= threshold
662
+ end
663
+
664
+ children = add_indentation(cumulative.map(&:rstrip).join(",\n"), 2)
665
+ lines << add_indentation(render_cursor(cursor, "enum_decl", :children => children).strip, 2)
666
+ end
667
+
668
+ # else branch: unversioned constants only (may be empty, but enum must still be defined)
669
+ base = versions[nil] || []
670
+ lines << "else"
671
+ children = add_indentation(base.map(&:rstrip).join(",\n"), 2)
672
+ lines << add_indentation(render_cursor(cursor, "enum_decl", :children => children).strip, 2)
673
+ lines << "end"
674
+ lines.join("\n")
675
+ end
676
+
677
+ def render_cursor(cursor, template, local_variables = {})
678
+ render_template(template, local_variables.merge(:cursor => cursor))
679
+ end
680
+
681
+ def render_callback(name, parameter_types, result_type)
682
+ render_template("callback", :ruby => name, :parameter_types => parameter_types,
683
+ :result_type => result_type)
684
+ end
685
+ end
686
+ end
687
+ end
@@ -0,0 +1 @@
1
+ :<%= cursor.ruby_name.empty? ? "unnamed_#{cursor.semantic_parent.map {|child, parent| child}.index(cursor)}" : cursor.ruby_name %>, <%= ffi_type %>
@@ -0,0 +1,5 @@
1
+ <%- if signature -%>
2
+ attach_function :<%= cursor.ruby_name %>, :<%= cursor.spelling %>, <%= signature -%>
3
+ <%- else -%>
4
+ attach_function :<%= cursor.ruby_name %>, :<%= cursor.spelling %>, [<%= parameter_types.join(", ") %>], <%= figure_ffi_type(cursor.result_type, :function) -%>
5
+ <%- end -%>