ffidb 0.12.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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/AUTHORS +1 -0
  3. data/CHANGES.md +7 -0
  4. data/CREDITS.md +2 -0
  5. data/README.md +201 -0
  6. data/UNLICENSE +24 -0
  7. data/VERSION +1 -0
  8. data/bin/ffidb +387 -0
  9. data/etc/mappings/dart.yaml +35 -0
  10. data/etc/mappings/java.yaml +36 -0
  11. data/etc/mappings/lisp.yaml +35 -0
  12. data/etc/mappings/python.yaml +35 -0
  13. data/etc/mappings/ruby.yaml +35 -0
  14. data/etc/templates/c.erb +46 -0
  15. data/etc/templates/cpp.erb +45 -0
  16. data/etc/templates/dart.erb +64 -0
  17. data/etc/templates/go.erb +50 -0
  18. data/etc/templates/java.erb +56 -0
  19. data/etc/templates/lisp.erb +49 -0
  20. data/etc/templates/python.erb +59 -0
  21. data/etc/templates/ruby.erb +48 -0
  22. data/lib/ffidb.rb +34 -0
  23. data/lib/ffidb/enum.rb +37 -0
  24. data/lib/ffidb/errors.rb +64 -0
  25. data/lib/ffidb/exporter.rb +141 -0
  26. data/lib/ffidb/exporters.rb +28 -0
  27. data/lib/ffidb/exporters/c.rb +52 -0
  28. data/lib/ffidb/exporters/cpp.rb +13 -0
  29. data/lib/ffidb/exporters/csharp.rb +6 -0
  30. data/lib/ffidb/exporters/csv.rb +24 -0
  31. data/lib/ffidb/exporters/dart.rb +60 -0
  32. data/lib/ffidb/exporters/go.rb +16 -0
  33. data/lib/ffidb/exporters/haskell.rb +3 -0
  34. data/lib/ffidb/exporters/java.rb +39 -0
  35. data/lib/ffidb/exporters/json.rb +38 -0
  36. data/lib/ffidb/exporters/julia.rb +3 -0
  37. data/lib/ffidb/exporters/lisp.rb +41 -0
  38. data/lib/ffidb/exporters/luajit.rb +3 -0
  39. data/lib/ffidb/exporters/nim.rb +4 -0
  40. data/lib/ffidb/exporters/nodejs.rb +4 -0
  41. data/lib/ffidb/exporters/ocaml.rb +4 -0
  42. data/lib/ffidb/exporters/php.rb +4 -0
  43. data/lib/ffidb/exporters/python.rb +35 -0
  44. data/lib/ffidb/exporters/racket.rb +3 -0
  45. data/lib/ffidb/exporters/ruby.rb +33 -0
  46. data/lib/ffidb/exporters/rust.rb +5 -0
  47. data/lib/ffidb/exporters/yaml.rb +31 -0
  48. data/lib/ffidb/exporters/zig.rb +3 -0
  49. data/lib/ffidb/function.rb +70 -0
  50. data/lib/ffidb/glob.rb +28 -0
  51. data/lib/ffidb/header.rb +19 -0
  52. data/lib/ffidb/header_parser.rb +339 -0
  53. data/lib/ffidb/library.rb +120 -0
  54. data/lib/ffidb/library_parser.rb +132 -0
  55. data/lib/ffidb/location.rb +17 -0
  56. data/lib/ffidb/parameter.rb +35 -0
  57. data/lib/ffidb/registry.rb +87 -0
  58. data/lib/ffidb/release.rb +14 -0
  59. data/lib/ffidb/struct.rb +41 -0
  60. data/lib/ffidb/symbol_table.rb +90 -0
  61. data/lib/ffidb/symbolic.rb +67 -0
  62. data/lib/ffidb/sysexits.rb +21 -0
  63. data/lib/ffidb/type.rb +214 -0
  64. data/lib/ffidb/typedef.rb +38 -0
  65. data/lib/ffidb/union.rb +37 -0
  66. data/lib/ffidb/version.rb +21 -0
  67. metadata +197 -0
@@ -0,0 +1,3 @@
1
+ # This is free and unencumbered software released into the public domain.
2
+
3
+ # TODO: https://ziglang.org/#Integration-with-C-libraries-without-FFIbindings
@@ -0,0 +1,70 @@
1
+ # This is free and unencumbered software released into the public domain.
2
+
3
+ require_relative 'location'
4
+ require_relative 'symbolic'
5
+
6
+ require 'pathname'
7
+ require 'yaml'
8
+
9
+ module FFIDB
10
+ class Function < ::Struct.new(:name, :type, :parameters, :definition, :comment, keyword_init: true)
11
+ include Symbolic
12
+
13
+ alias_method :result_type, :type
14
+ alias_method :return_type, :type
15
+
16
+ ##
17
+ # @return [Boolean]
18
+ def function?() return true end
19
+
20
+ ##
21
+ # @return [Boolean]
22
+ def public?() self.name[0] != '_' end
23
+
24
+ ##
25
+ # @return [Boolean]
26
+ def nonpublic?() !(self.public?) end
27
+
28
+ ##
29
+ # @return [Boolean]
30
+ def nullary?() self.arity.zero? end
31
+
32
+ ##
33
+ # @return [Boolean]
34
+ def unary?() self.arity.equal?(1) end
35
+
36
+ ##
37
+ # @return [Boolean]
38
+ def binary?() self.arity.equal?(2) end
39
+
40
+ ##
41
+ # @return [Boolean]
42
+ def ternary?() self.arity.equal?(3) end
43
+
44
+ ##
45
+ # @return [Integer]
46
+ def arity() self.parameters.size end
47
+
48
+ ##
49
+ # @return [Hash<Symbol, Object>]
50
+ def to_h
51
+ {
52
+ name: self.name.to_s,
53
+ type: self.type.to_s,
54
+ parameters: self.parameters&.transform_values { |v| v.type.to_s },
55
+ definition: self.definition&.to_h,
56
+ comment: self.comment,
57
+ }.delete_if { |k, v| v.nil? }
58
+ end
59
+
60
+ ##
61
+ # @return [String]
62
+ def to_yaml
63
+ h = self.to_h
64
+ h.delete(:parameters) if h[:parameters].empty?
65
+ h.transform_keys!(&:to_s)
66
+ h.transform_values! { |v| v.is_a?(Hash) ? v.transform_keys!(&:to_s) : v }
67
+ YAML.dump(h).gsub!("---\n", "--- !#{self.kind}\n")
68
+ end
69
+ end # Function
70
+ end # FFIDB
@@ -0,0 +1,28 @@
1
+ # This is free and unencumbered software released into the public domain.
2
+
3
+ module FFIDB
4
+ class Glob
5
+ attr_reader :pattern
6
+ attr_reader :compiled
7
+
8
+ def initialize(pattern, ignore_case: nil, match_substring: nil)
9
+ @pattern = pattern.to_s
10
+ regexp_pattern = Regexp.escape(@pattern).gsub('\*', '.*').gsub('\?', '.')
11
+ regexp_pattern = "^#{regexp_pattern}$" unless match_substring
12
+ regexp_options = ignore_case ? Regexp::IGNORECASE : nil
13
+ @compiled = Regexp.new(regexp_pattern, regexp_options)
14
+ end
15
+
16
+ ##
17
+ # @return [String]
18
+ def to_s
19
+ self.pattern
20
+ end
21
+
22
+ ##
23
+ # @return [Boolean]
24
+ def ===(string)
25
+ self.compiled === string
26
+ end
27
+ end # Glob
28
+ end # FFIDB
@@ -0,0 +1,19 @@
1
+ # This is free and unencumbered software released into the public domain.
2
+
3
+ require_relative 'symbol_table'
4
+
5
+ require 'pathname'
6
+
7
+ module FFIDB
8
+ class Header < ::Struct.new(:name, :comment, :typedefs, :enums, :structs, :unions, :functions, keyword_init: true)
9
+ include SymbolTable
10
+ include Comparable
11
+
12
+ ##
13
+ # @param [Header] other
14
+ # @return [Integer]
15
+ def <=>(other)
16
+ self.name <=> other.name
17
+ end
18
+ end # Header
19
+ end # FFIDB
@@ -0,0 +1,339 @@
1
+ # This is free and unencumbered software released into the public domain.
2
+
3
+ require_relative 'enum'
4
+ require_relative 'function'
5
+ require_relative 'header'
6
+ require_relative 'struct'
7
+ require_relative 'typedef'
8
+ require_relative 'union'
9
+
10
+ require 'pathname'
11
+
12
+ module FFIDB
13
+ class HeaderParser
14
+ attr_reader :base_directory
15
+ attr_reader :debug
16
+ attr_reader :defines
17
+ attr_reader :include_paths
18
+ attr_reader :include_symbols
19
+ attr_reader :exclude_symbols
20
+
21
+ ##
22
+ # @param [Pathname, #to_s] base_directory
23
+ def initialize(base_directory: nil, debug: nil)
24
+ require 'ffi/clang' # https://rubygems.org/gems/ffi-clang
25
+
26
+ @base_directory = base_directory
27
+ @debug = debug
28
+ @defines = {}
29
+ @include_paths = []
30
+ @include_symbols = {}
31
+ @exclude_symbols = {}
32
+ @clang_index = FFI::Clang::Index.new
33
+ end
34
+
35
+ ##
36
+ # @param [String, #to_s] var_and_val
37
+ # @return [void]
38
+ def parse_macro!(var_and_val)
39
+ var, val = var_and_val.to_s.split('=', 2)
40
+ val = 1 if val.nil?
41
+ self.define_macro! var, val
42
+ end
43
+
44
+ ##
45
+ # @param [Symbol, #to_sym] var
46
+ # @param [String, #to_s] val
47
+ # @return [void]
48
+ def define_macro!(var, val = 1)
49
+ self.defines[var.to_sym] = val.to_s
50
+ end
51
+
52
+ ##
53
+ # @param [Pathname, #to_s] path
54
+ # @return [void]
55
+ def add_include_path!(path)
56
+ self.include_paths << Pathname(path)
57
+ end
58
+
59
+ ##
60
+ # @param [Pathname, #to_s] path
61
+ # @yield [exception]
62
+ # @raise [ParsePanic] if parsing encounters a fatal error
63
+ # @return [Header]
64
+ def parse_header(path)
65
+ path = Pathname(path.to_s) unless path.is_a?(Pathname)
66
+ name = (self.base_directory ? path.relative_path_from(self.base_directory) : path).to_s
67
+ args = self.defines.inject([]) { |r, (k, v)| r << "-D#{k}=#{v}" }
68
+ args += self.include_paths.map { |p| "-I#{p}" }
69
+
70
+ translation_unit = nil
71
+ begin
72
+ translation_unit = @clang_index.parse_translation_unit(path.to_s, args)
73
+ rescue FFI::Clang::Error => error
74
+ raise ParsePanic.new(error.to_s)
75
+ end
76
+
77
+ translation_unit.diagnostics.each do |diagnostic|
78
+ exception_class = case diagnostic.severity.to_sym
79
+ when :fatal then raise ParsePanic.new(diagnostic.format)
80
+ when :error then ParseError
81
+ when :warning then ParseWarning
82
+ else ParseWarning
83
+ end
84
+ yield exception_class.new(diagnostic.format)
85
+ end
86
+
87
+ okayed_files = {}
88
+ FFIDB::Header.new(name: name, typedefs: [], enums: [], structs: [], unions: [], functions: []).tap do |header|
89
+ root_cursor = translation_unit.cursor
90
+ root_cursor.visit_children do |declaration, _|
91
+ location = declaration.location
92
+ location_file = location.file
93
+ if (okayed_files[location_file] ||= self.consider_path?(location_file))
94
+ case declaration.kind
95
+ when :cursor_typedef_decl
96
+ typedef = self.parse_typedef(declaration) do |symbol|
97
+ case
98
+ when symbol.enum? then header.enums << symbol
99
+ when symbol.struct? then header.structs << symbol
100
+ when symbol.union? then header.unions << symbol
101
+ end
102
+ end
103
+ header.typedefs << typedef if typedef
104
+ when :cursor_enum_decl
105
+ enum_name = declaration.spelling
106
+ if enum_name && !enum_name.empty?
107
+ header.enums << self.parse_enum(declaration)
108
+ end
109
+ when :cursor_struct
110
+ struct_name = declaration.spelling
111
+ if struct_name && !struct_name.empty?
112
+ if (struct = self.parse_struct(declaration))
113
+ header.structs << struct
114
+ end
115
+ end
116
+ when :cursor_union
117
+ union_name = declaration.spelling
118
+ if union_name && !union_name.empty?
119
+ if (union = self.parse_union(declaration))
120
+ header.unions << union
121
+ end
122
+ end
123
+ when :cursor_function
124
+ function_name = declaration.spelling
125
+ if self.consider_function?(function_name)
126
+ function = self.parse_function(declaration)
127
+ function.definition = self.parse_location(location)
128
+ header.functions << function
129
+ end
130
+ else # TODO: other declarations of interest?
131
+ end
132
+ end
133
+ :continue # visit the next sibling
134
+ end
135
+ header.comment = root_cursor.comment&.text
136
+ end
137
+ end
138
+
139
+ ##
140
+ # @param [FFI::Clang::Cursor] declaration
141
+ # @return [Typedef]
142
+ def parse_typedef(declaration, &block)
143
+ typedef_name = declaration.spelling
144
+ typedef_type = nil
145
+ declaration.visit_children do |node, _|
146
+ node_name = node.spelling
147
+ case node.kind
148
+ when :cursor_type_ref
149
+ typedef_type = node_name
150
+ when :cursor_enum_decl
151
+ typedef_type = "enum #{node_name}".rstrip
152
+ yield self.parse_enum(node, typedef_name: typedef_name)
153
+ when :cursor_struct
154
+ typedef_type = "struct #{node_name}".rstrip
155
+ yield self.parse_struct(node, typedef_name: typedef_name)
156
+ when :cursor_union
157
+ typedef_type = "union #{node_name}".rstrip
158
+ #yield self.parse_union(node, typedef_name: typedef_name) # TODO
159
+ end
160
+ :continue # visit the next sibling
161
+ end
162
+ FFIDB::Typedef.new(typedef_name, typedef_type) if typedef_type
163
+ end
164
+
165
+ ##
166
+ # @param [FFI::Clang::Cursor] declaration
167
+ # @param [String] typedef_name
168
+ # @return [Enum]
169
+ def parse_enum(declaration, typedef_name: nil)
170
+ enum_name = declaration.spelling
171
+ enum_name = typedef_name if enum_name.empty?
172
+ FFIDB::Enum.new(enum_name).tap do |enum|
173
+ declaration.visit_children do |node, _|
174
+ case node.kind
175
+ when :cursor_enum_constant_decl
176
+ k = node.spelling
177
+ v = node.enum_value
178
+ enum.values[k] = v
179
+ end
180
+ :continue # visit the next sibling
181
+ end
182
+ end
183
+ end
184
+
185
+ ##
186
+ # @param [FFI::Clang::Cursor] declaration
187
+ # @param [String] typedef_name
188
+ # @return [Struct]
189
+ def parse_struct(declaration, typedef_name: nil)
190
+ struct_name = declaration.spelling
191
+ struct_name = typedef_name if struct_name.empty?
192
+ FFIDB::Struct.new(struct_name).tap do |struct|
193
+ declaration.visit_children do |node, _|
194
+ case node.kind
195
+ when :cursor_field_decl
196
+ field_name = node.spelling
197
+ field_type = nil
198
+ node.visit_children do |node, _|
199
+ case node.kind
200
+ when :cursor_type_ref
201
+ field_type = node.spelling
202
+ :break
203
+ else :continue
204
+ end
205
+ end
206
+ struct.fields[field_name.to_sym] = Type.for(field_type)
207
+ end
208
+ :continue # visit the next sibling
209
+ end
210
+ end
211
+ end
212
+
213
+ ##
214
+ # @param [FFI::Clang::Cursor] declaration
215
+ # @param [String] typedef_name
216
+ # @return [Union]
217
+ def parse_union(declaration, typedef_name: nil)
218
+ # TODO: parse union declarations
219
+ end
220
+
221
+ ##
222
+ # @param [FFI::Clang::Cursor] declaration
223
+ # @return [Function]
224
+ def parse_function(declaration)
225
+ name = declaration.spelling
226
+ comment = declaration.comment&.text
227
+ function = FFIDB::Function.new(
228
+ name: name,
229
+ type: self.parse_type(declaration.type.result_type),
230
+ parameters: {},
231
+ definition: nil, # set in #parse_header()
232
+ comment: comment && !(comment.empty?) ? comment : nil,
233
+ )
234
+ declaration.visit_children do |node, _|
235
+ case node.kind
236
+ when :cursor_parm_decl
237
+ default_name = "_#{function.parameters.size + 1}"
238
+ parameter = self.parse_parameter(node, default_name: default_name)
239
+ function.parameters[parameter.name.to_sym] = parameter
240
+ end
241
+ :continue # visit the next sibling
242
+ end
243
+ function.parameters.freeze
244
+ function.instance_variable_set(:@debug, declaration.type.spelling.sub(/\s*\(/, " #{name}(")) if self.debug # TODO: __attribute__((noreturn))
245
+ function
246
+ end
247
+
248
+ ##
249
+ # @param [FFI::Clang::Cursor] declaration
250
+ # @param [String, #to_s] default_name
251
+ # @return [Parameter]
252
+ def parse_parameter(declaration, default_name: '_')
253
+ name = declaration.spelling
254
+ type = self.parse_type(declaration.type)
255
+ FFIDB::Parameter.new(
256
+ ((name.nil? || name.empty?) ? default_name.to_s : name).to_sym, type)
257
+ end
258
+
259
+ ##
260
+ # @param [FFI::Clang::Type] type
261
+ # @return [Type]
262
+ def parse_type(type)
263
+ ostensible_type = type.spelling
264
+ ostensible_type.sub!(/\*const$/, '*') # remove private const qualifiers
265
+ pointer_suffix = case ostensible_type
266
+ when /(\s\*+)$/
267
+ ostensible_type.delete_suffix!($1)
268
+ $1
269
+ else nil
270
+ end
271
+ resolved_type = if self.preserve_type?(ostensible_type)
272
+ ostensible_type << pointer_suffix if pointer_suffix
273
+ ostensible_type
274
+ else
275
+ type.canonical.spelling
276
+ end
277
+ resolved_type.sub!(/\*const$/, '*') # remove private const qualifiers
278
+ Type.for(resolved_type)
279
+ end
280
+
281
+ ##
282
+ # @param [String, #to_s] type_name
283
+ # @return [Boolean]
284
+ def preserve_type?(type_name)
285
+ case type_name.to_s
286
+ when 'va_list' then true # <stdarg.h>
287
+ when '_Bool' then true # <stdbool.h>
288
+ when 'size_t', 'wchar_t' then true # <stddef.h>
289
+ when 'const size_t', 'const wchar_t' then true # <stddef.h> # FIXME: need a better solution
290
+ when /^u?int\d+_t$/, /^u?int\d+_t \*$/ then true # <stdint.h>
291
+ when /^u?intptr_t$/ then true # <stdint.h>
292
+ when 'FILE' then true # <stdio.h>
293
+ when 'ssize_t', 'off_t', 'off64_t' then true # <sys/types.h>
294
+ else false
295
+ end
296
+ end
297
+
298
+ ##
299
+ # @param [FFI::Clang::ExpansionLocation] location
300
+ # @return [Location]
301
+ def parse_location(location)
302
+ return nil if location.nil?
303
+ FFIDB::Location.new(
304
+ file: location.file ? self.make_relative_path(location.file).to_s : nil,
305
+ line: location.line,
306
+ )
307
+ end
308
+
309
+ protected
310
+
311
+ ##
312
+ # @param [String, #to_s] function_name
313
+ # @return [Boolean]
314
+ def consider_function?(function_name)
315
+ function_name = function_name.to_s
316
+ if not self.include_symbols.empty?
317
+ self.include_symbols[function_name]
318
+ else
319
+ !self.exclude_symbols[function_name]
320
+ end
321
+ end
322
+
323
+ ##
324
+ # @param [Pathname, #to_s] path
325
+ # @return [Boolean]
326
+ def consider_path?(path)
327
+ path = Pathname(path) unless path.is_a?(Pathname)
328
+ path.expand_path.to_s.start_with?(base_directory.expand_path.to_s << '/')
329
+ end
330
+
331
+ ##
332
+ # @param [Pathname, #to_s] path
333
+ # @return [Pathname]
334
+ def make_relative_path(path)
335
+ path = Pathname(path) unless path.is_a?(Pathname)
336
+ self.base_directory ? path.relative_path_from(self.base_directory) : path
337
+ end
338
+ end # HeaderParser
339
+ end # FFIDB