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,65 @@
1
+ module RubyBindgen
2
+ class NameMapper
3
+ attr_reader :exact, :regex
4
+ protected :exact, :regex
5
+
6
+ def initialize(mappings = [])
7
+ @exact = {}
8
+ @regex = []
9
+ mappings.each do |pattern, replacement|
10
+ if pattern.is_a?(Regexp)
11
+ @regex << [pattern, replacement]
12
+ else
13
+ @exact[pattern] = replacement
14
+ end
15
+ end
16
+ end
17
+
18
+ # Factory: parses YAML config array of {from:, to:} entries
19
+ def self.from_config(mappings)
20
+ parsed = mappings.filter_map do |entry|
21
+ key = entry[:from] || entry["from"]
22
+ replacement = entry[:to] || entry["to"]
23
+ next if key.nil?
24
+ if key.start_with?('/') && key.end_with?('/') && key.length > 2
25
+ [Regexp.new(key[1..-2]), replacement]
26
+ else
27
+ [key, replacement]
28
+ end
29
+ end
30
+ new(parsed)
31
+ end
32
+
33
+ # Look up a name, trying each candidate in order.
34
+ # Returns the replacement value or nil.
35
+ def lookup(*candidates)
36
+ # O(1) exact match
37
+ candidates.each do |name|
38
+ result = @exact[name]
39
+ return result if result
40
+ end
41
+
42
+ # Regex fallback
43
+ @regex.each do |pattern, replacement|
44
+ candidates.each do |name|
45
+ if (m = pattern.match(name))
46
+ if replacement.is_a?(String)
47
+ return replacement.gsub(/\\(\d+)/) { m[$1.to_i] }
48
+ else
49
+ return replacement
50
+ end
51
+ end
52
+ end
53
+ end
54
+ nil
55
+ end
56
+
57
+ # Merge two tables. Other's entries override self's.
58
+ def merge(other)
59
+ exact_mappings = @exact.merge(other.exact).map { |k, v| [k, v] }
60
+ regex_mappings = other.regex + @regex
61
+ self.class.new(exact_mappings + regex_mappings)
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,138 @@
1
+ module RubyBindgen
2
+ class Namer
3
+ def initialize(rename_types = NameMapper.new, rename_methods = NameMapper.new,
4
+ conversion_mappings = NameMapper.new)
5
+ @rename_types = rename_types
6
+ @rename_methods = rename_methods
7
+ @conversion_mappings = conversion_mappings
8
+ end
9
+
10
+ def ruby(cursor)
11
+ if cursor.anonymous? && cursor.kind == :cursor_namespace
12
+ "Anonymous"
13
+ elsif cursor.anonymous?
14
+ definer = cursor.anonymous_definer
15
+ definer ? definer.spelling.camelize : nil
16
+ else
17
+ case cursor.kind
18
+ when :cursor_translation_unit
19
+ basename = File.basename(cursor.spelling, File.extname(cursor.spelling))
20
+ basename.camelize
21
+ when :cursor_conversion_function
22
+ ruby_conversion_function(cursor)
23
+ when :cursor_function, :cursor_cxx_method
24
+ ruby_operator_or_method(cursor)
25
+ when :cursor_struct, :cursor_union, :cursor_enum_decl, :cursor_class_decl, :cursor_namespace
26
+ @rename_types.lookup(cursor.spelling) || cursor.spelling.camelize
27
+ when :cursor_field_decl
28
+ cursor.spelling.underscore
29
+ when :cursor_typedef_decl
30
+ cursor.underlying_type.declaration.invalid? ?
31
+ cursor.spelling.underscore :
32
+ ruby(cursor.underlying_type.declaration)
33
+ when :cursor_variable
34
+ cursor.spelling.camelize
35
+ else
36
+ cursor.spelling.underscore
37
+ end
38
+ end
39
+ end
40
+
41
+ def cruby(cursor)
42
+ case cursor.kind
43
+ when :cursor_class_decl
44
+ "rb_c#{cursor.type.spelling.sub("(anonymous namespace)", "Anonymous").camelize}"
45
+ when :cursor_struct
46
+ "rb_c#{cursor.type.spelling.camelize}"
47
+ when :cursor_enum_decl
48
+ "rb_c#{cursor.type.spelling.sub("(anonymous namespace)", "Anonymous").camelize}"
49
+ when :cursor_namespace
50
+ if cursor.anonymous?
51
+ # qualified_name is nil for translation units
52
+ "rb_m#{cursor.semantic_parent.qualified_name&.camelize}Anonymous"
53
+ else
54
+ "rb_m#{cursor.qualified_name.camelize}"
55
+ end
56
+ when :cursor_typedef_decl
57
+ "rb_c#{cursor.spelling.sub("(anonymous namespace)", "Anonymous").camelize}"
58
+ when :cursor_translation_unit
59
+ "Class(rb_cObject)"
60
+ else
61
+ cursor.spelling.underscore
62
+ end
63
+ end
64
+
65
+ # Apply rename_types to a generated Ruby class name.
66
+ # Accepts one or more candidate names to try (e.g., raw C++ name, camelized name).
67
+ # Returns the first match, or the last candidate as fallback.
68
+ def apply_rename_types(*names)
69
+ @rename_types.lookup(*names) || names.last
70
+ end
71
+
72
+ private
73
+
74
+ # Handle conversion functions like operator int(), operator float()
75
+ def ruby_conversion_function(cursor)
76
+ # Use result_type.spelling to get the original typedef name (e.g., "size_t")
77
+ # rather than the resolved type from cursor.spelling (e.g., "unsigned long")
78
+ type_name = cursor.type.result_type.spelling
79
+
80
+ # Look up Ruby convention for this type
81
+ suffix = @conversion_mappings.lookup(type_name)
82
+ if suffix
83
+ "to_#{suffix}"
84
+ else
85
+ # Handle std::basic_string variants (std::string is a typedef)
86
+ if type_name.include?('basic_string')
87
+ return "to_s"
88
+ end
89
+
90
+ # Clean up the type name for Ruby method naming:
91
+ # - Remove reference/pointer markers
92
+ # - Use only the final type name (after last ::)
93
+ # - Convert to underscore style
94
+ clean_name = type_name.gsub(/[&*]/, '').strip
95
+ clean_name = clean_name.split('::').last || clean_name
96
+ # Remove template parameters for cleaner method names
97
+ clean_name = clean_name.sub(/<.*>$/, '')
98
+ "to_#{clean_name.underscore}"
99
+ end
100
+ end
101
+
102
+ # Handle operators and regular methods
103
+ def ruby_operator_or_method(cursor)
104
+ # Check rename_methods first (includes operator mappings merged by generator).
105
+ # SymbolCandidates yields the cursor spelling, qualified-name forms (with
106
+ # the inline-namespace-collapsed parent fallback), template display
107
+ # variants, and parameter-list forms — the same set Symbols uses, so a
108
+ # rename rule expressed in any form Symbols would accept also matches here.
109
+ symbol_candidates = SymbolCandidates.new(cursor)
110
+ result = @rename_methods.lookup(*symbol_candidates)
111
+
112
+ case result
113
+ when String then return result
114
+ when Proc then return result.call(cursor)
115
+ end
116
+
117
+ # No mapping — apply heuristics for non-operators only
118
+ spelling = cursor.spelling
119
+ unless spelling.start_with?('operator')
120
+ is_bool = cursor.type.result_type.spelling == "bool"
121
+ # Methods starting with "is" prefix (isFoo or is_foo) are predicates
122
+ is_prefixed = spelling.match?(/^is[A-Z_]/)
123
+
124
+ # Add ? suffix for predicate methods:
125
+ # 1. bool return with no parameters, OR
126
+ # 2. bool return with "is" prefix (regardless of parameters)
127
+ if is_bool && (cursor.type.args_size == 0 || is_prefixed)
128
+ return "#{spelling.underscore.sub(/^is_/, "")}?"
129
+ else
130
+ return spelling.underscore
131
+ end
132
+ end
133
+
134
+ # Unknown operator with no mapping
135
+ raise "Unknown operator: #{spelling}"
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ module RubyBindgen
7
+ class Outputter
8
+ attr_reader :base_path, :output_paths
9
+
10
+ def initialize(base_path)
11
+ @base_path = base_path
12
+ @output_paths = {}
13
+ end
14
+
15
+ def output_path(relative_path)
16
+ File.expand_path(File.join(self.base_path, relative_path))
17
+ end
18
+
19
+ def write(relative_path, content)
20
+ path = self.output_path(relative_path)
21
+ cleaned = cleanup_whitespace(content)
22
+ FileUtils.mkdir_p(File.dirname(path))
23
+ File.open(path, "wb") do |file|
24
+ file << cleaned
25
+ end
26
+ @output_paths[path] = cleaned
27
+ end
28
+
29
+ private
30
+
31
+ # Clean up whitespace issues in generated content:
32
+ # - Collapse multiple consecutive blank lines to single blank line
33
+ # - Remove blank lines before closing braces
34
+ def cleanup_whitespace(content)
35
+ content = content.gsub(/\n{3,}/, "\n\n")
36
+ content = content.gsub(/\n\n(\s*\})/, "\n\\1")
37
+ content
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module RubyBindgen
6
+ class Parser
7
+ class ParseError < RuntimeError
8
+ attr_reader :path, :details
9
+
10
+ def initialize(path, details: [])
11
+ @path = path
12
+ @details = details
13
+ super(build_message(path, details))
14
+ end
15
+
16
+ private
17
+
18
+ def build_message(path, details)
19
+ return "Failed to parse: #{path}" if details.empty?
20
+
21
+ formatted_details = details.map { |detail| " #{detail}" }.join("\n")
22
+ "Parse errors in #{path}:\n#{formatted_details}"
23
+ end
24
+ end
25
+
26
+ attr_reader :inputter, :clang_args
27
+
28
+ def initialize(inputter, clang_args, libclang: nil)
29
+ @inputter = inputter
30
+ @clang_args = clang_args
31
+
32
+ # Set libclang path before loading ffi-clang (it reads ENV on load)
33
+ ENV['LIBCLANG'] = libclang if libclang
34
+
35
+ # Lazy-load ffi-clang and its refinements so CMake format doesn't need libclang
36
+ require 'ffi/clang'
37
+ require 'ruby-bindgen/refinements/cursor'
38
+
39
+ @index = FFI::Clang::Index.new(exclude_declarations_from_pch: false, display_diagnostics: true)
40
+ end
41
+
42
+ def generate(visitor)
43
+ visitor.visit_start
44
+
45
+ STDOUT << "\n" << "Processing:" << "\n"
46
+ self.inputter.each do |path, relative_path|
47
+ STDOUT << " " << path << "\n"
48
+ begin
49
+ translation_unit = parse_translation_unit(path)
50
+ rescue ParseError => error
51
+ raise unless visitor.respond_to?(:visit_parse_error)
52
+
53
+ visitor.visit_parse_error(path, relative_path, error)
54
+ next
55
+ end
56
+ visitor.visit_translation_unit(translation_unit, path, relative_path)
57
+ end
58
+
59
+ visitor.visit_end
60
+ end
61
+
62
+ private
63
+
64
+ def parse_translation_unit(path)
65
+ translation_unit = @index.parse_translation_unit(path, self.clang_args, [],
66
+ [:detailed_preprocessing_record, :skip_function_bodies])
67
+
68
+ raise ParseError.new(path) if translation_unit.nil?
69
+
70
+ check_diagnostics(translation_unit, path)
71
+ translation_unit
72
+ end
73
+
74
+ def check_diagnostics(translation_unit, path)
75
+ errors = translation_unit.diagnostics.select { |d| d.severity == :fatal || d.severity == :error }
76
+ return if errors.empty?
77
+
78
+ details = errors.map { |d| "#{d.severity}: #{d.spelling}" }
79
+ raise ParseError.new(path, details: details)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,57 @@
1
+ module FFI
2
+ module Clang
3
+ class Cursor
4
+ def self.namer
5
+ @namer || raise("Namer not set — generator must call Cursor.namer= before processing")
6
+ end
7
+
8
+ def self.namer=(value)
9
+ @namer = value
10
+ end
11
+
12
+ def ruby_name
13
+ self.class.namer.ruby(self)
14
+ end
15
+
16
+ def cruby_name
17
+ self.class.namer.cruby(self)
18
+ end
19
+
20
+ def anonymous_definer
21
+ return nil unless self.anonymous?
22
+
23
+ if self.kind == :cursor_namespace
24
+ return self
25
+ end
26
+
27
+ # This could be a typedef of a field declaration in union or struct
28
+ #
29
+ # typedef struct {
30
+ # union {
31
+ # char *sdata;
32
+ # int idata;
33
+ # } u;
34
+ # } F_TextItemT;
35
+ _, result = self.translation_unit.cursor.find do |child, parent|
36
+ self.eql?(child) && (parent.kind == :cursor_field_decl ||
37
+ parent.kind == :cursor_typedef_decl)
38
+ end
39
+
40
+ # Or this could be a variable declaration
41
+ #
42
+ # struct {
43
+ # int Value;
44
+ # uint8_t String[4];
45
+ # } MyArray_t;
46
+ unless result
47
+ variables = self.translation_unit.cursor.find_by_kind(true, :cursor_variable)
48
+ result = variables.find do |variable|
49
+ self.eql?(variable.type.declaration)
50
+ end
51
+ end
52
+ result
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,41 @@
1
+ class String
2
+ # Taken from ActiveSupport with modifications
3
+ def upcase_first
4
+ if self.length > 0
5
+ self[0].upcase.concat(self[1..-1])
6
+ else
7
+ self
8
+ end
9
+ end
10
+
11
+ # Taken from ActiveSupport with modifications
12
+ def camelize()
13
+ if self.match?(/\A[a-z\d]*\z/)
14
+ return self.capitalize
15
+ end
16
+
17
+ return self if self.match?(/\A[A-Z]+\z/)
18
+
19
+ input = self.match?(/\A[A-Z_0-9]*\z/) ? self.downcase : self
20
+ string = input.sub(/^[a-z\d]*/) { |match| match.capitalize! || match }
21
+ string.gsub!(/\/, ::/)
22
+ string.gsub!(/(?:_|-|\.|::|,| |\<|\>|(\/))([a-z\d]*)/i) do
23
+ word = $2
24
+ word[0] = word[0].capitalize || word[0] unless word.empty?
25
+ $1 ? "::#{word}" : word
26
+ end
27
+ string
28
+ end
29
+
30
+ # Taken from ActiveSupport with modifications
31
+ def underscore
32
+ return self unless /[A-Z-]|::/.match?(self)
33
+ word = self.gsub("::".freeze, "/".freeze)
34
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
35
+ word.gsub!(/([a-z])([A-Z])/, '\1_\2'.freeze)
36
+ word.gsub!(/([a-z])(\d+[A-Z])/, '\1_\2'.freeze)
37
+ word.tr!("-".freeze, "_".freeze)
38
+ word.downcase!
39
+ word
40
+ end
41
+ end