ffidb 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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