c2ffi4rb 0.0.1 → 0.0.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a7511b48117f836d65991de99d5aeaf1a4dc5f4c9a9b99e4a9577fe415c941a
4
- data.tar.gz: 3ee454048f080abae9a07ab3a11293d1af7a8f57518d6ae1ae12eaac6fd7d2ab
3
+ metadata.gz: 06ea739a36f2f4a6485ba3e65b6e91724eafed7f7440df1d26e7dfb5d5290897
4
+ data.tar.gz: 2eaf74991d02fd8b6e5beae79150cc1b40f5178c4bb27cf1277bf1bae0c744f2
5
5
  SHA512:
6
- metadata.gz: b75a019a77528b7ceac118bc3108991c0cae5aba439ef330a1480fe6dd7390ce04e5780f7263dfab9dd626f98bf754166a9780272277267098e5e1871625d9fe
7
- data.tar.gz: 3aa7cf6cf49094f75d9ff220c90e66edda3b80092fe96d087c1471b22f71d9d9c5b56a3f5bab914ec0db908222f90c7d0be26f2c9e6eac0a406b806b82f56936
6
+ metadata.gz: cdc2f8439c2722a83b3a82aac50821d4bf4f9be35efa52e30202e44417ab29b3a203896ddc955e0046637667d50751a354902955df92560c22a4b74a9939fc61
7
+ data.tar.gz: 5bb0f1ba5929c3ba8dbc4ca176701fddc05a3fb836ac218a1606b935335af035972354cfb4ba0950f5a19daf04457486801fca9ea868685070c1134b90e6938b
data/README.md CHANGED
@@ -3,7 +3,6 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/c2ffi4rb.svg)](https://badge.fury.io/rb/c2ffi4rb)
4
4
  [![test](https://github.com/kojix2/c2ffi4rb/actions/workflows/ci.yml/badge.svg)](https://github.com/kojix2/c2ffi4rb/actions/workflows/ci.yml)
5
5
 
6
-
7
6
  [c2ffi](https://github.com/rpav/c2ffi) - FFI binding generator - for Ruby
8
7
 
9
8
  ## installation
@@ -14,21 +13,29 @@ gem install c2ffi4rb
14
13
 
15
14
  ## Usage
16
15
 
17
- First, produce a `spec` file using `c2ffi`:
16
+ First, produce a `spec` file using `c2ffi`.
18
17
 
19
18
  ```sh
20
19
  c2ffi -M macros.h -o example.json example.h
21
- c2ffi -o macros.json macros.h
20
+ c2ffi -o macros.json example.h macros.h
22
21
  ```
23
22
 
24
- Now you can generate a file manually with the included tool,
25
- `c2ffi-ruby`, as follows:
23
+ Next, use c2ffi4rb to generate ruby code.
26
24
 
27
25
  ```sh
28
26
  c2ffi4rb example.json macro.json > simple.rb
29
27
  ```
30
28
 
31
- This produces the `simple.rb` file, as included.
29
+ Finally, improve simple.rb manually to complete the binding.
30
+
31
+ ## Development
32
+
33
+ To run tests, install [c2ffi][c2ffi], [Cairo][cairo], and [pkg-config][pkg-config] in advance.
34
+ Then execute `rake test`.
35
+
36
+ [cairo]: https://www.cairographics.org/ "Cairo"
37
+ [pkg-config]: https://www.freedesktop.org/wiki/Software/pkg-config/ "freedesktop.org"
38
+ [c2ffi]: https://github.com/rpav/c2ffi "GitHub"
32
39
 
33
40
  ## Licence
34
41
 
data/exe/c2ffi4rb CHANGED
@@ -8,16 +8,28 @@
8
8
  require 'optparse'
9
9
  require 'c2ffi4rb'
10
10
 
11
- options = {}
12
-
13
- Version = C2FFI4RB::VERSION # Fixme
11
+ type_table = {}
14
12
 
15
13
  opts = OptionParser.new
16
14
  opts.banner = 'Usage: c2ffi4rb [options] [file1, file2, ...]'
15
+ opts.on('-t FILE', '--table FILE', 'Type conversion table TSV') do |file|
16
+ File.open(file) do |io|
17
+ io.each_line do |line|
18
+ next if line =~ /^#/
19
+
20
+ from, to = line.chomp.split("\t")
21
+ type_table[from] = to
22
+ end
23
+ end
24
+ end
17
25
  opts.on('-h', '--help', 'Show this message') do
18
26
  puts opts
19
27
  exit
20
28
  end
29
+ opts.on('-v', '--version', 'Show version') do
30
+ puts C2FFI4RB::VERSION
31
+ exit
32
+ end
21
33
  opts.parse!
22
34
 
23
35
  if ARGV.empty?
@@ -32,4 +44,4 @@ ARGV.each do |file|
32
44
  end
33
45
  end
34
46
 
35
- C2FFI4RB::Parser.parse(spec)
47
+ C2FFI4RB::BindGen.new(type_table).generate_bindings(spec)
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module C2FFI4RB
6
+ class BindGen
7
+ DEFAULT_TYPE_TABLE = {
8
+ ':signed-int' => ':int',
9
+ ':unsigned-int' => ':uint',
10
+ ':signed-char' => ':char',
11
+ ':unsigned-char' => ':uchar',
12
+ ':signed-short' => ':short',
13
+ ':unsigned-short' => ':ushort',
14
+ ':long-long' => ':long_long',
15
+ ':ulong-long' => ':ulong_long',
16
+ ':long-double' => ':long_double',
17
+ ':signed-long' => ':long',
18
+ ':unsigned-long' => ':ulong',
19
+ ':unsigned-long-long' => ':ulong_long',
20
+ ':function-pointer' => ':pointer'
21
+ }
22
+
23
+ def self.generate_bindings(arr)
24
+ new.generate_bindings(arr)
25
+ end
26
+
27
+ def initialize(type_table = {})
28
+ @type_table = DEFAULT_TYPE_TABLE.merge(type_table)
29
+ @struct_type = []
30
+ @toplevels = []
31
+ @anon_counter = 0
32
+ @anon_enum_ids = []
33
+ @typedefs = {}
34
+ end
35
+
36
+ def generate_bindings(arr)
37
+ arr.each { |form| process_toplevel(form) }
38
+ puts @toplevels.join("\n\n")
39
+ end
40
+
41
+ private
42
+
43
+ def process_toplevel(form)
44
+ lines = parse_form(form)
45
+ @toplevels << lines
46
+ end
47
+
48
+ def parse_form(form)
49
+ case form[:tag]
50
+ when 'typedef' then generate_typedef(form)
51
+ when 'const' then generate_const(form)
52
+ when 'extern' then generate_extern(form)
53
+ when 'function' then generate_function(form)
54
+ when 'struct', 'union' then generate_struct_or_union(form)
55
+ when 'enum' then generate_enum(form)
56
+ else
57
+ raise "Unknown form: #{form[:tag]}"
58
+ end
59
+ end
60
+
61
+ def generate_typedef(form)
62
+ type = resolve_type(form[:type])
63
+ if @struct_type.include?(type)
64
+ name = define_struct(form[:name])
65
+ if name == type
66
+ "# #{name} = #{type}"
67
+ else
68
+ "#{name} = #{type}"
69
+ end
70
+ elsif type == ":{#{form[:name]}}"
71
+ warn "Ignoring self-referential typedef #{form[:name]}"
72
+ else
73
+ if @typedefs.has_key?(form[:name])
74
+ return "# typedef already defined? #{form[:name]}" if @typedefs[form[:name]] == type
75
+
76
+ warn "# Redefinition of #{form[:name]} from #{@typedefs[form[:name]]} to #{type}"
77
+
78
+ end
79
+ "typedef :pointer, :#{form[:name]} # #{type}" if form[:type][:tag] == ':array'
80
+ @typedefs[form[:name]] = type
81
+ "typedef #{type}, :#{form[:name]}"
82
+ end
83
+ end
84
+
85
+ def generate_const(form)
86
+ type = resolve_type(form[:type])
87
+ value = type == ':string' ? "\"#{form[:value]}\"" : form[:value]
88
+ "#{form[:name].upcase} = #{value}"
89
+ end
90
+
91
+ def generate_extern(form)
92
+ "attach_variable :#{form[:name]}, :#{form[:name]}, #{resolve_type(form[:type])}"
93
+ end
94
+
95
+ def generate_function(form)
96
+ params = form[:parameters].map do |f|
97
+ f[:type][:tag] == ':array' ? " :pointer, # #{resolve_type(f[:type])}" : " #{resolve_type(f[:type])},"
98
+ end.join("\n")
99
+ <<~FUNCTION
100
+ attach_function '#{form[:name]}', [
101
+ #{params}
102
+ ], #{resolve_type(form[:'return-type'])}
103
+ FUNCTION
104
+ end
105
+
106
+ def generate_struct_or_union(form)
107
+ create_struct_definition(form)
108
+ end
109
+
110
+ def generate_enum(form)
111
+ id = form[:id]
112
+ if form[:name].empty?
113
+ return "# Already defined? anon_type_#{id}" if @anon_enum_ids.include?(id)
114
+
115
+ @anon_enum_ids << id
116
+ end
117
+ name = normalize_enum_name(form[:name], form[:id])
118
+ fields = form[:fields].map { |f| " :#{f[:name]}, #{f[:value]}," }.join("\n")
119
+ <<~ENUM
120
+ enum #{name}, [
121
+ #{fields}
122
+ ]
123
+ ENUM
124
+ end
125
+
126
+ def normalize_struct_name(name)
127
+ # Anonymous structs are given a name
128
+ if name.empty?
129
+ @anon_counter += 1
130
+ name = "Anon_Type_#{@anon_counter}"
131
+ return name
132
+ end
133
+
134
+ # Do not allow names that start with an underscore
135
+ name = 'C' + name if name.start_with?('_')
136
+
137
+ # Convert snake_case to CamelCase
138
+ name = name[0].upcase + name[1..-1]
139
+
140
+ name.gsub!(/_([a-z])/) { |m| "#{m[1].upcase}" }
141
+
142
+ name
143
+ end
144
+
145
+ def register_struct(name)
146
+ if @struct_type.include? name
147
+ false
148
+ else
149
+ @struct_type << name
150
+ end
151
+ end
152
+
153
+ def normalize_enum_name(name, id)
154
+ # Anonymous enums are given a name
155
+ name = "anon_enum_#{id}" if name.empty?
156
+
157
+ # All enums are prefixed with a colon
158
+ name = ":#{name}" unless name.start_with?(':')
159
+ name
160
+ end
161
+
162
+ def define_struct(name)
163
+ name = normalize_struct_name(name)
164
+ register_struct(name)
165
+ name
166
+ end
167
+
168
+ def create_struct_definition(form)
169
+ name = normalize_struct_name(form[:name])
170
+ # FIXME: This is a hack to avoid redefining structs
171
+ return "# Already defined? #{name}" if @struct_type.include?(name)
172
+
173
+ register_struct(name)
174
+
175
+ # FIXME: :FFI::Struct or FFI::Struct
176
+ type = form[:tag] == 'struct' ? '::FFI::Struct' : '::FFI::Union'
177
+
178
+ lines = ["class #{name} < #{type}"]
179
+
180
+ if form[:fields].any?
181
+ lines << ' layout \\'
182
+ anon_field_counter = 0
183
+
184
+ form[:fields].each_with_index do |f, i|
185
+ field_name = f[:name].empty? ? "anon_field_#{anon_field_counter}" : f[:name]
186
+ anon_field_counter += 1 if f[:name].empty?
187
+
188
+ sep = i == form[:fields].size - 1 ? '' : ','
189
+ lines << " :#{field_name}, #{resolve_type(f[:type])}#{sep}"
190
+ end
191
+ end
192
+
193
+ lines << 'end'
194
+ lines.join("\n")
195
+ end
196
+
197
+ def resolve_type(form)
198
+ @type_table.fetch(form[:tag]) do
199
+ case form[:tag]
200
+ when ':pointer' then resolve_pointer_type(form)
201
+ when ':array' then resolve_array_type(form)
202
+ when ':struct', ':union' then define_struct(form[:name])
203
+ when ':enum' then normalize_enum_name(form[:name], form[:id])
204
+ when 'struct', 'union' then define_struct_or_union_type(form)
205
+ when 'enum' then normalize_enum_name_type(form)
206
+ else resolve_default_type(form)
207
+ end
208
+ end
209
+ end
210
+
211
+ def resolve_pointer_type(form)
212
+ pointee = resolve_type(form[:type])
213
+ if [':char', ':uchar'].include?(pointee)
214
+ ':string'
215
+ elsif @struct_type.include?(pointee)
216
+ "#{pointee}.ptr"
217
+ else
218
+ ':pointer'
219
+ end
220
+ end
221
+
222
+ def resolve_array_type(form)
223
+ "[#{resolve_type(form[:type])}, #{form[:size]}]"
224
+ end
225
+
226
+ def normalize_enum_name_type(form)
227
+ form[:name] = normalize_enum_name(form[:name], form[:id])
228
+ process_toplevel(form)
229
+ form[:name]
230
+ end
231
+
232
+ def define_struct_or_union_type(form)
233
+ form[:name] = normalize_struct_name(form[:name])
234
+ process_toplevel(form)
235
+ form[:name]
236
+ end
237
+
238
+ def resolve_default_type(form)
239
+ ## FIXME
240
+ st_name = normalize_struct_name(form[:tag])
241
+ return st_name if @struct_type.include?(st_name)
242
+
243
+ form[:tag].start_with?(':') ? form[:tag] : ":#{form[:tag]}"
244
+ end
245
+ end
246
+ end
@@ -1,3 +1,3 @@
1
1
  module C2FFI4RB
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.3'
3
3
  end
data/lib/c2ffi4rb.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'c2ffi4rb/version'
4
- require_relative 'c2ffi4rb/parser'
4
+ require_relative 'c2ffi4rb/bind_gen'
5
5
 
6
6
  module C2FFI4RB
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: c2ffi4rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - kojix2
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-09-18 00:00:00.000000000 Z
12
+ date: 2024-09-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -65,7 +65,7 @@ files:
65
65
  - README.md
66
66
  - exe/c2ffi4rb
67
67
  - lib/c2ffi4rb.rb
68
- - lib/c2ffi4rb/parser.rb
68
+ - lib/c2ffi4rb/bind_gen.rb
69
69
  - lib/c2ffi4rb/version.rb
70
70
  homepage: https://github.com/kojix2/c2ffi4rb
71
71
  licenses:
@@ -86,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
86
  - !ruby/object:Gem::Version
87
87
  version: '0'
88
88
  requirements: []
89
- rubygems_version: 3.4.14
89
+ rubygems_version: 3.5.11
90
90
  signing_key:
91
91
  specification_version: 4
92
92
  summary: C2FFI for Ruby-FFI
@@ -1,187 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module C2FFI4RB
6
- class Parser
7
- TYPE_TABLE = {
8
- ':unsigned-int' => ':uint',
9
- ':unsigned-char' => ':uchar',
10
- ':unsigned-long' => ':ulong',
11
- ':function-pointer' => ':pointer'
12
- }
13
-
14
- def self.parse(arr)
15
- Parser.new.parse(arr)
16
- end
17
-
18
- def initialize
19
- @struct_type = []
20
- @toplevels = []
21
- @anon_counter = 0
22
- end
23
-
24
- def parse(arr)
25
- arr.each do |form|
26
- parse_toplevel(form)
27
- end
28
-
29
- puts @toplevels.join("\n\n")
30
- end
31
-
32
- private
33
-
34
- def parse_toplevel(form)
35
- lines = \
36
- case form[:tag]
37
- when 'typedef'
38
- type = parse_type(form[:type])
39
-
40
- # I don't think typedef works right with structs, so assign
41
- if @struct_type.include?(type)
42
- name = add_struct(form[:name])
43
- "#{name} = #{type}"
44
- else
45
- "typedef #{type}, :#{form[:name]}"
46
- end
47
-
48
- when 'const'
49
- type = parse_type(form[:type])
50
- if type == ':string'
51
- "#{form[:name].upcase} = \"#{form[:value]}\""
52
- else
53
- "#{form[:name].upcase} = #{form[:value]}"
54
- end
55
-
56
- when 'extern'
57
- 'attach_variable ' \
58
- ":#{form[:name]}, :#{form[:name]}, #{parse_type(form[:type])}"
59
-
60
- when 'function'
61
- l = []
62
- l << "attach_function '#{form[:name]}', ["
63
- form[:parameters].each do |f|
64
- l << " #{parse_type(f[:type])},"
65
- end
66
- l << "], #{parse_type(form['return-type'.intern])}"
67
- # emacs doesn't like :"foo" ---^
68
- l.join("\n")
69
-
70
- when 'struct', 'union'
71
- make_struct(form)
72
-
73
- when 'enum'
74
- name = add_enum(form[:name])
75
- l = []
76
- l << "enum #{name}, ["
77
- form[:fields].each do |f|
78
- l << " :#{f[:name]}, #{f[:value]},"
79
- end
80
- l << ']'
81
- l.join("\n")
82
- end
83
-
84
- @toplevels << lines
85
- nil
86
- end
87
-
88
- def add_struct(name)
89
- # Anonymous structs are given a name
90
- if name == ''
91
- @anon_counter += 1
92
- name = "Anon_Type_#{@anon_counter}"
93
- return name
94
- end
95
-
96
- # Do not allow names that start with an underscore
97
- name = 'C' + name if name.start_with? '_'
98
-
99
- # Convert snake_case to CamelCase
100
- name = name.capitalize.gsub!(/_([a-z])/) { |m| "_#{m[1].upcase}" }
101
-
102
- @struct_type << name unless @struct_type.include? name
103
- name
104
- end
105
-
106
- def add_enum(name)
107
- # Anonymous enums are given a name
108
- if name == ''
109
- @anon_counter += 1
110
- name = ':anon_type_' + @anon_counter.to_s
111
- return name
112
- end
113
-
114
- # All enums are prefixed with a colon
115
- name = ":#{name}" unless name.start_with? ':'
116
- name
117
- end
118
-
119
- def make_struct(form)
120
- name = add_struct(form[:name])
121
-
122
- type = if form[:tag] == ':struct'
123
- 'FFI::Struct'
124
- else
125
- 'FFI::Union'
126
- end
127
-
128
- l = []
129
- l << "class #{name} < #{type}"
130
-
131
- if form[:fields].length.positive?
132
- l << ' layout \\'
133
- size = form[:fields].length
134
- form[:fields].each_with_index do |f, i|
135
- sep = i >= (size - 1) ? '' : ','
136
- l << " :#{f[:name]}, #{parse_type(f[:type])}#{sep}"
137
- end
138
- end
139
- l << 'end'
140
- l.join("\n")
141
- end
142
-
143
- def parse_type(form)
144
- tt = TYPE_TABLE[form[:tag]]
145
- return tt if tt
146
-
147
- case form[:tag]
148
- when ':pointer'
149
- pointee = parse_type(form[:type])
150
- if [':char', ':uchar'].include?(pointee)
151
- ':string'
152
- elsif @struct_type.include?(pointee)
153
- "#{pointee}.ptr"
154
- else
155
- ':pointer'
156
- end
157
-
158
- when ':array'
159
- "[#{parse_type(form[:type])}, #{form[:size]}]"
160
-
161
- when ':struct', ':union'
162
- add_struct(form[:name])
163
-
164
- when ':enum'
165
- add_enum(form[:name])
166
-
167
- when 'enum'
168
- form[:name] = add_enum(form[:name])
169
- parse_toplevel(form)
170
- form[:name]
171
-
172
- when 'struct', 'union'
173
- form[:name] = add_struct(form[:name])
174
- parse_toplevel(form)
175
- form[:name]
176
-
177
- else
178
- # All non-Classy types are :-prefixed?
179
- if form[:tag][0] != ':'
180
- ":#{form[:tag]}"
181
- else
182
- form[:tag]
183
- end
184
- end
185
- end
186
- end
187
- end