qt 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +27 -0
- data/README.md +303 -0
- data/Rakefile +94 -0
- data/examples/development_ordered_demos/01_dsl_hello.rb +22 -0
- data/examples/development_ordered_demos/02_live_layout_console.rb +137 -0
- data/examples/development_ordered_demos/03_component_showcase.rb +235 -0
- data/examples/development_ordered_demos/04_paint_simple.rb +147 -0
- data/examples/development_ordered_demos/05_tetris_simple.rb +295 -0
- data/examples/development_ordered_demos/06_timetrap_clockify.rb +759 -0
- data/examples/development_ordered_demos/07_peek_like_recorder.rb +597 -0
- data/examples/qtproject/widgets/itemviews/spreadsheet/main.rb +252 -0
- data/examples/qtproject/widgets/widgetsgallery/main.rb +184 -0
- data/ext/qt_ruby_bridge/extconf.rb +75 -0
- data/ext/qt_ruby_bridge/qt_ruby_runtime.hpp +23 -0
- data/ext/qt_ruby_bridge/runtime_events.cpp +408 -0
- data/ext/qt_ruby_bridge/runtime_signals.cpp +212 -0
- data/lib/qt/application_lifecycle.rb +44 -0
- data/lib/qt/bridge.rb +95 -0
- data/lib/qt/children_tracking.rb +15 -0
- data/lib/qt/constants.rb +10 -0
- data/lib/qt/date_time_codec.rb +104 -0
- data/lib/qt/errors.rb +6 -0
- data/lib/qt/event_runtime.rb +139 -0
- data/lib/qt/event_runtime_dispatch.rb +35 -0
- data/lib/qt/event_runtime_qobject_methods.rb +41 -0
- data/lib/qt/generated_constants_runtime.rb +33 -0
- data/lib/qt/inspectable.rb +29 -0
- data/lib/qt/key_sequence_codec.rb +22 -0
- data/lib/qt/native.rb +93 -0
- data/lib/qt/shortcut_compat.rb +30 -0
- data/lib/qt/string_codec.rb +44 -0
- data/lib/qt/variant_codec.rb +78 -0
- data/lib/qt/version.rb +5 -0
- data/lib/qt.rb +47 -0
- data/scripts/generate_bridge/ast_introspection.rb +267 -0
- data/scripts/generate_bridge/auto_method_spec_resolver.rb +37 -0
- data/scripts/generate_bridge/auto_methods.rb +438 -0
- data/scripts/generate_bridge/core_utils.rb +114 -0
- data/scripts/generate_bridge/cpp_method_return_emitter.rb +93 -0
- data/scripts/generate_bridge/ffi_api.rb +46 -0
- data/scripts/generate_bridge/free_function_specs.rb +289 -0
- data/scripts/generate_bridge/spec_discovery.rb +313 -0
- data/scripts/generate_bridge.rb +1113 -0
- metadata +99 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
def unsupported_cpp_type?(type_name)
|
|
4
|
+
type_name.include?('<') || type_name.include?('>') || type_name.include?('[') || type_name.include?('(')
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def map_cpp_pointer_arg_type(type_name, qt_class)
|
|
8
|
+
return nil unless type_name.end_with?('*')
|
|
9
|
+
|
|
10
|
+
base = type_name.sub(/\s*\*\z/, '').strip
|
|
11
|
+
if qt_class && !base.include?('::') && base.match?(/\A[A-Z]\w*\z/) && !base.start_with?('Q')
|
|
12
|
+
base = "#{qt_class}::#{base}"
|
|
13
|
+
end
|
|
14
|
+
{ ffi: :pointer, cast: "#{base}*" }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def map_builtin_intlike_arg_type(type_name)
|
|
18
|
+
return { ffi: :int } if type_name == 'int'
|
|
19
|
+
return { ffi: :bool } if type_name == 'bool'
|
|
20
|
+
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def map_qualified_intlike_arg_type(type_name, qt_class, int_cast_types)
|
|
25
|
+
return nil unless qt_class && type_name.match?(/\A[A-Z]\w*\z/)
|
|
26
|
+
|
|
27
|
+
qualified = "#{qt_class}::#{type_name}"
|
|
28
|
+
return { ffi: :int, cast: qualified } if int_cast_types&.include?(qualified)
|
|
29
|
+
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def map_cpp_intlike_arg_type(type_name, qt_class, int_cast_types)
|
|
34
|
+
builtin = map_builtin_intlike_arg_type(type_name)
|
|
35
|
+
return builtin if builtin
|
|
36
|
+
return { ffi: :int, cast: type_name } if type_name.include?('::') && int_cast_types&.include?(type_name)
|
|
37
|
+
|
|
38
|
+
map_qualified_intlike_arg_type(type_name, qt_class, int_cast_types)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def map_cpp_arg_type(type_name, qt_class: nil, int_cast_types: nil)
|
|
42
|
+
raw = type_name.to_s.strip
|
|
43
|
+
return nil if raw.end_with?('&') && !raw.start_with?('const ')
|
|
44
|
+
|
|
45
|
+
compact_raw = raw.gsub(/\s+/, ' ')
|
|
46
|
+
|
|
47
|
+
type = raw
|
|
48
|
+
type = type.sub(/\Aconst\s+/, '').sub(/\s*&\z/, '').strip
|
|
49
|
+
return nil if unsupported_cpp_type?(type)
|
|
50
|
+
return { ffi: :string, cast: :qstring } if type == 'QString'
|
|
51
|
+
return { ffi: :string, cast: :qdatetime_from_utf8 } if type == 'QDateTime'
|
|
52
|
+
return { ffi: :string, cast: :qdate_from_utf8 } if type == 'QDate'
|
|
53
|
+
return { ffi: :string, cast: :qtime_from_utf8 } if type == 'QTime'
|
|
54
|
+
return { ffi: :string, cast: :qkeysequence_from_utf8 } if type == 'QKeySequence'
|
|
55
|
+
return { ffi: :pointer, cast: :qicon_ref } if type == 'QIcon'
|
|
56
|
+
return { ffi: :string, cast: :qany_string_view } if type == 'QAnyStringView'
|
|
57
|
+
return { ffi: :string, cast: :qvariant_from_utf8 } if type == 'QVariant'
|
|
58
|
+
return { ffi: :string } if compact_raw.match?(/\Aconst\s+char\s*\*\z/)
|
|
59
|
+
|
|
60
|
+
map_cpp_pointer_arg_type(type, qt_class) || map_cpp_intlike_arg_type(type, qt_class, int_cast_types)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def normalized_cpp_type_name(type_name)
|
|
64
|
+
type = type_name.to_s.strip
|
|
65
|
+
type = type.sub(/\Aconst\s+/, '').sub(/\s*&\z/, '').strip
|
|
66
|
+
type = type.sub(/\s*\*\z/, '*') if type.end_with?('*')
|
|
67
|
+
type
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def map_cpp_return_type(type_name)
|
|
71
|
+
raw = type_name.to_s.strip
|
|
72
|
+
return nil if unsupported_cpp_type?(raw)
|
|
73
|
+
return nil if raw.start_with?('const ') && raw.end_with?('*')
|
|
74
|
+
|
|
75
|
+
type = raw.sub(/\Aconst\s+/, '').sub(/\s*&\z/, '').strip
|
|
76
|
+
map_scalar_cpp_return_type(type) || map_pointer_cpp_return_type(type)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def map_scalar_cpp_return_type(type)
|
|
80
|
+
return { ffi_return: :void } if type == 'void'
|
|
81
|
+
return { ffi_return: :int } if type == 'int'
|
|
82
|
+
return { ffi_return: :bool } if type == 'bool'
|
|
83
|
+
return { ffi_return: :string, return_cast: :qstring_to_utf8 } if type == 'QString'
|
|
84
|
+
return { ffi_return: :string, return_cast: :qdatetime_to_utf8 } if type == 'QDateTime'
|
|
85
|
+
return { ffi_return: :string, return_cast: :qdate_to_utf8 } if type == 'QDate'
|
|
86
|
+
return { ffi_return: :string, return_cast: :qtime_to_utf8 } if type == 'QTime'
|
|
87
|
+
return { ffi_return: :string, return_cast: :qvariant_to_utf8 } if type == 'QVariant'
|
|
88
|
+
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def map_pointer_cpp_return_type(type)
|
|
93
|
+
return { ffi_return: :pointer } if type.end_with?('*')
|
|
94
|
+
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def parse_method_param_nodes(method_decl)
|
|
99
|
+
Array(method_decl['inner']).select { |node| node['kind'] == 'ParmVarDecl' }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def parse_method_param(param, idx)
|
|
103
|
+
{
|
|
104
|
+
name: param['name'] || "arg#{idx + 1}",
|
|
105
|
+
type: param.dig('type', 'qualType').to_s,
|
|
106
|
+
has_default: !param['init'].nil?
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parse_method_params(method_decl)
|
|
111
|
+
params = parse_method_param_nodes(method_decl)
|
|
112
|
+
required_arg_count = params.count { |param| param['init'].nil? }
|
|
113
|
+
parsed_params = params.each_with_index.map { |param, idx| parse_method_param(param, idx) }
|
|
114
|
+
[parsed_params, required_arg_count]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def parse_method_signature(method_decl)
|
|
118
|
+
qual = method_decl.dig('type', 'qualType').to_s
|
|
119
|
+
md = qual.match(/\A(.+?)\s*\((.*)\)/)
|
|
120
|
+
return nil unless md
|
|
121
|
+
|
|
122
|
+
parsed_params, required_arg_count = parse_method_params(method_decl)
|
|
123
|
+
{
|
|
124
|
+
return_type: md[1].strip,
|
|
125
|
+
required_arg_count: required_arg_count,
|
|
126
|
+
params: parsed_params
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_auto_method_args(parsed, entry, qt_class, int_cast_types)
|
|
131
|
+
arg_cast_overrides = Array(entry[:arg_casts])
|
|
132
|
+
params = parsed[:params]
|
|
133
|
+
required_arg_count = 0
|
|
134
|
+
args = []
|
|
135
|
+
|
|
136
|
+
params.each_with_index do |param, idx|
|
|
137
|
+
cast_override = arg_cast_overrides[idx]
|
|
138
|
+
arg_info = map_cpp_arg_type(param[:type], qt_class: qt_class, int_cast_types: int_cast_types)
|
|
139
|
+
arg_info ||= { ffi: :int } if cast_override
|
|
140
|
+
unless arg_info
|
|
141
|
+
return nil unless skip_unsupported_optional_tail?(params, idx, param)
|
|
142
|
+
|
|
143
|
+
break
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
args << { name: param[:name], ffi: arg_info[:ffi], cast: cast_override || arg_info[:cast] }.compact
|
|
147
|
+
required_arg_count += 1 unless param[:has_default]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
[args, required_arg_count]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def skip_unsupported_optional_tail?(params, idx, param)
|
|
154
|
+
return false unless param[:has_default]
|
|
155
|
+
|
|
156
|
+
params[(idx + 1)..].all? { |rest| rest[:has_default] }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def build_auto_method_hash(entry, ret_info, args, required_arg_count)
|
|
160
|
+
method = {
|
|
161
|
+
qt_name: entry[:qt_name],
|
|
162
|
+
ruby_name: ruby_public_method_name(entry[:qt_name], entry[:ruby_name]),
|
|
163
|
+
ffi_return: ret_info[:ffi_return],
|
|
164
|
+
args: args,
|
|
165
|
+
required_arg_count: required_arg_count
|
|
166
|
+
}
|
|
167
|
+
method[:return_cast] = ret_info[:return_cast] if ret_info[:return_cast]
|
|
168
|
+
method
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def build_auto_method_from_decl(method_decl, entry, qt_class:, int_cast_types:)
|
|
172
|
+
parsed = parse_method_signature(method_decl)
|
|
173
|
+
return nil unless parsed
|
|
174
|
+
|
|
175
|
+
ret_info = map_cpp_return_type(parsed[:return_type])
|
|
176
|
+
return nil unless ret_info
|
|
177
|
+
|
|
178
|
+
args, required_arg_count = build_auto_method_args(parsed, entry, qt_class, int_cast_types)
|
|
179
|
+
return nil unless args
|
|
180
|
+
|
|
181
|
+
build_auto_method_hash(entry, ret_info, args, required_arg_count)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def valid_auto_exportable_identifier?(name)
|
|
185
|
+
return false if name.nil? || name.empty?
|
|
186
|
+
return false unless name.match?(/\A[A-Za-z_]\w*\z/)
|
|
187
|
+
|
|
188
|
+
true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def forbidden_auto_exportable_method_name?(name)
|
|
192
|
+
return true if name.start_with?('~')
|
|
193
|
+
return true if name.include?('operator')
|
|
194
|
+
return true if name.end_with?('Event')
|
|
195
|
+
return true if name.start_with?('qt_check_for_')
|
|
196
|
+
|
|
197
|
+
forbidden_names = %w[
|
|
198
|
+
event eventFilter childEvent customEvent timerEvent connectNotify disconnectNotify d_func connect disconnect
|
|
199
|
+
initialize
|
|
200
|
+
]
|
|
201
|
+
return true if forbidden_names.include?(name)
|
|
202
|
+
|
|
203
|
+
false
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def auto_exportable_method_name?(name)
|
|
207
|
+
return false unless valid_auto_exportable_identifier?(name)
|
|
208
|
+
return false if forbidden_auto_exportable_method_name?(name)
|
|
209
|
+
|
|
210
|
+
true
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def deprecated_method_decl?(decl)
|
|
214
|
+
Array(decl['inner']).any? { |node| node['kind'] == 'DeprecatedAttr' }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def method_names_cache_entry(ast, class_name)
|
|
218
|
+
@method_names_with_bases_cache ||= {}.compare_by_identity
|
|
219
|
+
per_ast = (@method_names_with_bases_cache[ast] ||= {})
|
|
220
|
+
[per_ast, class_name]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def invalid_method_name_scope?(class_name, visited)
|
|
224
|
+
class_name.nil? || class_name.empty? || visited[class_name]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def build_auto_method_candidate(decl, entry, qt_class, int_cast_types)
|
|
228
|
+
parsed = parse_method_signature(decl)
|
|
229
|
+
return nil unless parsed
|
|
230
|
+
|
|
231
|
+
method = build_auto_method_from_decl(decl, entry, qt_class: qt_class, int_cast_types: int_cast_types)
|
|
232
|
+
return nil unless method
|
|
233
|
+
|
|
234
|
+
{
|
|
235
|
+
method: method,
|
|
236
|
+
param_types: parsed[:params].map { |param| normalized_cpp_type_name(param[:type]) }
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def auto_method_decl_candidate?(decl)
|
|
241
|
+
return false unless decl['__effective_access'] == 'public'
|
|
242
|
+
return false if deprecated_method_decl?(decl)
|
|
243
|
+
return false unless auto_exportable_method_name?(decl['name'])
|
|
244
|
+
|
|
245
|
+
true
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def collect_method_names_with_bases(ast, class_name, visited = {})
|
|
249
|
+
cache, cache_key = method_names_cache_entry(ast, class_name)
|
|
250
|
+
return cache[cache_key] if cache[cache_key]
|
|
251
|
+
return [] if invalid_method_name_scope?(class_name, visited)
|
|
252
|
+
|
|
253
|
+
visited[class_name] = true
|
|
254
|
+
index = ast_class_index(ast)
|
|
255
|
+
own_names = index[:methods_by_class].fetch(class_name, {}).keys
|
|
256
|
+
base_names = collect_base_method_names_with_bases(ast, class_name, visited)
|
|
257
|
+
combined = (own_names + base_names).uniq
|
|
258
|
+
cache[cache_key] = combined
|
|
259
|
+
combined
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def collect_base_method_names_with_bases(ast, class_name, visited)
|
|
263
|
+
collect_class_bases(ast, class_name).flat_map do |base|
|
|
264
|
+
collect_method_names_with_bases(ast, base, visited)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def resolve_auto_method_cache_key(qt_class, entry)
|
|
269
|
+
[
|
|
270
|
+
qt_class,
|
|
271
|
+
entry[:qt_name],
|
|
272
|
+
entry[:ruby_name],
|
|
273
|
+
entry[:param_count],
|
|
274
|
+
Array(entry[:param_types]).map { |type_name| normalized_cpp_type_name(type_name) },
|
|
275
|
+
Array(entry[:arg_casts])
|
|
276
|
+
]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def build_auto_method_candidates(decls, entry, qt_class, int_cast_types)
|
|
280
|
+
decls.filter_map do |decl|
|
|
281
|
+
next unless auto_method_decl_candidate?(decl)
|
|
282
|
+
|
|
283
|
+
build_auto_method_candidate(decl, entry, qt_class, int_cast_types)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def filter_auto_method_candidates(candidates, entry)
|
|
288
|
+
filtered = candidates
|
|
289
|
+
|
|
290
|
+
if entry[:param_count]
|
|
291
|
+
filtered = filtered.select { |candidate| candidate[:method][:args].length == entry[:param_count] }
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if entry[:param_types]
|
|
295
|
+
expected = entry[:param_types].map { |type_name| normalized_cpp_type_name(type_name) }
|
|
296
|
+
filtered = filtered.select { |candidate| candidate[:param_types] == expected }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
filtered
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def resolve_auto_method_entry(auto_entry)
|
|
303
|
+
auto_entry.is_a?(String) ? { qt_name: auto_entry } : auto_entry.dup
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def resolve_auto_method_cached(cache, cache_key)
|
|
307
|
+
return [false, nil] unless cache.key?(cache_key)
|
|
308
|
+
|
|
309
|
+
[true, cache[cache_key]]
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def resolve_auto_method_cache(ast)
|
|
313
|
+
@resolve_auto_method_cache ||= {}.compare_by_identity
|
|
314
|
+
@resolve_auto_method_cache[ast] ||= {}
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def cached_auto_method(per_ast_cache, qt_class, entry)
|
|
318
|
+
cache_key = resolve_auto_method_cache_key(qt_class, entry)
|
|
319
|
+
cache_hit, cached = resolve_auto_method_cached(per_ast_cache, cache_key)
|
|
320
|
+
[cache_key, cache_hit, cached]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def resolve_auto_method_built_candidates(ast, qt_class, entry)
|
|
324
|
+
decls = collect_method_decls_with_bases(ast, qt_class, entry.fetch(:qt_name))
|
|
325
|
+
return nil if decls.empty?
|
|
326
|
+
|
|
327
|
+
int_cast_types = ast_int_cast_type_set(ast)
|
|
328
|
+
built = build_auto_method_candidates(decls, entry, qt_class, int_cast_types)
|
|
329
|
+
return nil if built.empty?
|
|
330
|
+
|
|
331
|
+
built = filter_auto_method_candidates(built, entry)
|
|
332
|
+
return nil if built.empty?
|
|
333
|
+
|
|
334
|
+
built
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def resolve_auto_method(ast, qt_class, auto_entry)
|
|
338
|
+
entry = resolve_auto_method_entry(auto_entry)
|
|
339
|
+
per_ast_cache = resolve_auto_method_cache(ast)
|
|
340
|
+
cache_key, cache_hit, cached = cached_auto_method(per_ast_cache, qt_class, entry)
|
|
341
|
+
return cached if cache_hit
|
|
342
|
+
|
|
343
|
+
built = resolve_auto_method_built_candidates(ast, qt_class, entry)
|
|
344
|
+
return per_ast_cache[cache_key] = nil unless built
|
|
345
|
+
|
|
346
|
+
per_ast_cache[cache_key] = built.min_by { |candidate| candidate[:method][:args].length }[:method]
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def auto_entries_for_spec(spec, ast)
|
|
350
|
+
auto_mode = spec[:auto_methods]
|
|
351
|
+
return Array(auto_mode) unless auto_mode == :all
|
|
352
|
+
|
|
353
|
+
names = collect_method_names_with_bases(ast, spec[:qt_class]).select { |name| auto_exportable_method_name?(name) }
|
|
354
|
+
rules = spec.fetch(:auto_method_rules, {})
|
|
355
|
+
names.sort.map do |name|
|
|
356
|
+
rule = rules[name.to_sym] || rules[name]
|
|
357
|
+
rule ? { qt_name: name }.merge(rule) : { qt_name: name }
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def resolve_auto_methods_for_spec(ast, spec, auto_entries, manual_methods, auto_mode)
|
|
362
|
+
resolver = build_auto_method_resolver(ast, spec, manual_methods, auto_mode)
|
|
363
|
+
spec_resolved = 0
|
|
364
|
+
spec_skipped = 0
|
|
365
|
+
auto_methods = auto_entries.filter_map do |entry|
|
|
366
|
+
method, spec_skipped, spec_resolved = resolve_with_resolver(resolver, entry, spec_skipped, spec_resolved)
|
|
367
|
+
method
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
[auto_methods, spec_resolved, spec_skipped]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def build_auto_method_resolver(ast, spec, manual_methods, auto_mode)
|
|
374
|
+
existing_names = manual_methods.to_set { |method| method[:qt_name] }
|
|
375
|
+
resolve_method = ->(entry) { resolve_auto_method(ast, spec[:qt_class], entry) }
|
|
376
|
+
AutoMethodSpecResolver.new(
|
|
377
|
+
spec: spec,
|
|
378
|
+
auto_mode: auto_mode,
|
|
379
|
+
existing_names: existing_names,
|
|
380
|
+
resolve_method: resolve_method
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def resolve_with_resolver(resolver, entry, spec_skipped, spec_resolved)
|
|
385
|
+
resolver.resolve(entry, skipped: spec_skipped, resolved: spec_resolved)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def expand_auto_methods(specs, ast)
|
|
389
|
+
totals = { candidates: 0, resolved: 0, skipped: 0 }
|
|
390
|
+
|
|
391
|
+
expanded_specs = specs.map do |spec|
|
|
392
|
+
expand_auto_methods_for_spec(spec, ast, totals)
|
|
393
|
+
end
|
|
394
|
+
debug_log("auto totals candidates=#{totals[:candidates]} resolved=#{totals[:resolved]} skipped=#{totals[:skipped]}")
|
|
395
|
+
expanded_specs
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def expand_auto_methods_for_spec(spec, ast, totals)
|
|
399
|
+
spec_start = monotonic_now
|
|
400
|
+
auto_mode = spec[:auto_methods]
|
|
401
|
+
auto_entries = auto_entries_for_spec(spec, ast)
|
|
402
|
+
manual_methods = Array(spec[:methods])
|
|
403
|
+
return spec if auto_entries.empty?
|
|
404
|
+
|
|
405
|
+
auto_result = resolve_spec_auto_methods(ast, spec, auto_entries, manual_methods, auto_mode)
|
|
406
|
+
apply_auto_method_result!(totals, spec, auto_mode, spec_start, auto_result)
|
|
407
|
+
spec.merge(methods: manual_methods + auto_result[:methods])
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def resolve_spec_auto_methods(ast, spec, auto_entries, manual_methods, auto_mode)
|
|
411
|
+
auto_methods, spec_resolved, spec_skipped = resolve_auto_methods_for_spec(
|
|
412
|
+
ast, spec, auto_entries, manual_methods, auto_mode
|
|
413
|
+
)
|
|
414
|
+
{ methods: auto_methods, candidates: auto_entries.length, resolved: spec_resolved, skipped: spec_skipped }
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def update_auto_method_totals!(totals, spec_candidates, spec_resolved, spec_skipped)
|
|
418
|
+
totals[:candidates] += spec_candidates
|
|
419
|
+
totals[:resolved] += spec_resolved
|
|
420
|
+
totals[:skipped] += spec_skipped
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def apply_auto_method_result!(totals, spec, auto_mode, spec_start, auto_result)
|
|
424
|
+
update_auto_method_totals!(
|
|
425
|
+
totals, auto_result[:candidates], auto_result[:resolved], auto_result[:skipped]
|
|
426
|
+
)
|
|
427
|
+
log_auto_method_expansion(spec: spec, auto_mode: auto_mode, counts: auto_result, spec_start: spec_start)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def log_auto_method_expansion(spec:, auto_mode:, counts:, spec_start:)
|
|
431
|
+
elapsed = monotonic_now - spec_start
|
|
432
|
+
message = "auto #{spec[:qt_class]} mode=#{auto_mode || :list} " \
|
|
433
|
+
"candidates=#{counts[:candidates]} resolved=#{counts[:resolved]} " \
|
|
434
|
+
"skipped=#{counts[:skipped]} #{format('%.3fs', elapsed)}"
|
|
435
|
+
debug_log(
|
|
436
|
+
message
|
|
437
|
+
)
|
|
438
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
def debug_enabled?
|
|
4
|
+
ENV['QT_RUBY_GENERATOR_DEBUG'] == '1'
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def monotonic_now
|
|
8
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def debug_log(message)
|
|
12
|
+
puts "[gen] #{message}" if debug_enabled?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def timed(label)
|
|
16
|
+
start = monotonic_now
|
|
17
|
+
value = yield
|
|
18
|
+
elapsed = monotonic_now - start
|
|
19
|
+
debug_log("#{label}=#{format('%.3fs', elapsed)}")
|
|
20
|
+
value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def required_includes(scope)
|
|
24
|
+
case scope
|
|
25
|
+
when 'widgets'
|
|
26
|
+
%w[QApplication QtWidgets]
|
|
27
|
+
when 'qobject', 'all'
|
|
28
|
+
%w[QApplication QtCore QtGui QtWidgets]
|
|
29
|
+
else
|
|
30
|
+
raise "Unsupported QT_RUBY_SCOPE=#{scope.inspect}. Supported: #{SUPPORTED_SCOPES.join(', ')}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ffi_to_cpp_type(ffi)
|
|
35
|
+
case ffi
|
|
36
|
+
when :pointer then 'void*'
|
|
37
|
+
when :string then 'const char*'
|
|
38
|
+
when :int then 'int'
|
|
39
|
+
when :bool then 'bool'
|
|
40
|
+
else
|
|
41
|
+
raise "Unsupported ffi type: #{ffi.inspect}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ffi_return_to_cpp(ffi)
|
|
46
|
+
case ffi
|
|
47
|
+
when :void then 'void'
|
|
48
|
+
when :pointer then 'void*'
|
|
49
|
+
when :int then 'int'
|
|
50
|
+
when :bool then 'bool'
|
|
51
|
+
when :string then 'const char*'
|
|
52
|
+
else
|
|
53
|
+
raise "Unsupported ffi return: #{ffi.inspect}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_snake(name)
|
|
58
|
+
name.gsub(/([a-z\d])([A-Z])/, '\\1_\\2').downcase
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def prefix_for_qt_class(qt_class)
|
|
62
|
+
core = qt_class.delete_prefix('Q')
|
|
63
|
+
"q#{to_snake(core)}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ruby_safe_method_name(name)
|
|
67
|
+
RUBY_RESERVED_WORDS.include?(name) ? "#{name}_" : name
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ruby_public_method_name(qt_name, explicit_name = nil)
|
|
71
|
+
base = explicit_name || qt_name
|
|
72
|
+
safe = ruby_safe_method_name(base)
|
|
73
|
+
return safe unless RUNTIME_RESERVED_RUBY_METHODS.include?(safe)
|
|
74
|
+
|
|
75
|
+
RUNTIME_METHOD_RENAMES.fetch(safe, "#{safe}_qt")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ruby_safe_arg_name(name, index, used)
|
|
79
|
+
base = name.to_s
|
|
80
|
+
base = "arg#{index + 1}" unless base.match?(/\A[A-Za-z_]\w*\z/)
|
|
81
|
+
base = "#{base}_arg" if RUBY_RESERVED_WORDS.include?(base)
|
|
82
|
+
|
|
83
|
+
candidate = base
|
|
84
|
+
counter = 1
|
|
85
|
+
candidate = "#{base}_#{counter += 1}" while used.include?(candidate)
|
|
86
|
+
used << candidate
|
|
87
|
+
candidate
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def ruby_arg_name_map(args)
|
|
91
|
+
used = Set.new
|
|
92
|
+
args.each_with_index.to_h { |arg, idx| [arg[:name], ruby_safe_arg_name(arg[:name], idx, used)] }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def lower_camel(name)
|
|
96
|
+
return name if name.empty?
|
|
97
|
+
|
|
98
|
+
name[0].downcase + name[1..]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def property_name_from_setter(qt_name)
|
|
102
|
+
return nil unless qt_name.start_with?('set')
|
|
103
|
+
return nil if qt_name.length <= 3
|
|
104
|
+
|
|
105
|
+
lower_camel(qt_name.delete_prefix('set'))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ctor_function_name(spec)
|
|
109
|
+
"qt_ruby_#{spec[:prefix]}_new"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def method_function_name(spec, method)
|
|
113
|
+
"qt_ruby_#{spec[:prefix]}_#{to_snake(method[:qt_name])}"
|
|
114
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Emits C++ return statements for generated bridge methods.
|
|
4
|
+
class CppMethodReturnEmitter
|
|
5
|
+
def initialize(lines:, method:, invocation:)
|
|
6
|
+
@lines = lines
|
|
7
|
+
@method = method
|
|
8
|
+
@invocation = invocation
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def emit
|
|
12
|
+
return emit_void if method[:ffi_return] == :void
|
|
13
|
+
return emit_qstring if qstring_return?
|
|
14
|
+
return emit_qdatetime if qdatetime_return?
|
|
15
|
+
return emit_qdate if qdate_return?
|
|
16
|
+
return emit_qtime if qtime_return?
|
|
17
|
+
return emit_qvariant if qvariant_return?
|
|
18
|
+
return emit_pointer if method[:ffi_return] == :pointer
|
|
19
|
+
|
|
20
|
+
emit_value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :lines, :method, :invocation
|
|
26
|
+
|
|
27
|
+
def qstring_return?
|
|
28
|
+
method[:ffi_return] == :string && method[:return_cast] == :qstring_to_utf8
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def qvariant_return?
|
|
32
|
+
method[:ffi_return] == :string && method[:return_cast] == :qvariant_to_utf8
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def qdatetime_return?
|
|
36
|
+
method[:ffi_return] == :string && method[:return_cast] == :qdatetime_to_utf8
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def qdate_return?
|
|
40
|
+
method[:ffi_return] == :string && method[:return_cast] == :qdate_to_utf8
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def qtime_return?
|
|
44
|
+
method[:ffi_return] == :string && method[:return_cast] == :qtime_to_utf8
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def emit_void
|
|
48
|
+
lines << " #{invocation};"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def emit_qstring
|
|
52
|
+
lines << " const QString value = #{invocation};"
|
|
53
|
+
lines << ' thread_local QByteArray utf8;'
|
|
54
|
+
lines << ' utf8 = value.toUtf8();'
|
|
55
|
+
lines << ' return utf8.constData();'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def emit_qvariant
|
|
59
|
+
lines << " const QVariant value = #{invocation};"
|
|
60
|
+
lines << ' thread_local QByteArray utf8;'
|
|
61
|
+
lines << ' utf8 = qvariant_to_bridge_string(value).toUtf8();'
|
|
62
|
+
lines << ' return utf8.constData();'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def emit_qdatetime
|
|
66
|
+
lines << " const QDateTime value = #{invocation};"
|
|
67
|
+
lines << ' thread_local QByteArray utf8;'
|
|
68
|
+
lines << ' utf8 = qdatetime_to_bridge_string(value).toUtf8();'
|
|
69
|
+
lines << ' return utf8.constData();'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def emit_qdate
|
|
73
|
+
lines << " const QDate value = #{invocation};"
|
|
74
|
+
lines << ' thread_local QByteArray utf8;'
|
|
75
|
+
lines << ' utf8 = qdate_to_bridge_string(value).toUtf8();'
|
|
76
|
+
lines << ' return utf8.constData();'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def emit_qtime
|
|
80
|
+
lines << " const QTime value = #{invocation};"
|
|
81
|
+
lines << ' thread_local QByteArray utf8;'
|
|
82
|
+
lines << ' utf8 = qtime_to_bridge_string(value).toUtf8();'
|
|
83
|
+
lines << ' return utf8.constData();'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def emit_pointer
|
|
87
|
+
lines << " return const_cast<void*>(static_cast<const void*>(#{invocation}));"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def emit_value
|
|
91
|
+
lines << " return #{invocation};"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
def free_functions(free_function_specs)
|
|
4
|
+
free_function_specs.map do |spec|
|
|
5
|
+
{ name: spec[:name], ffi_return: spec[:ffi_return], args: spec[:args] }
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def all_ffi_functions(specs, free_function_specs:)
|
|
10
|
+
fns = free_functions(free_function_specs).dup
|
|
11
|
+
|
|
12
|
+
specs.each do |spec|
|
|
13
|
+
append_constructor_ffi_function(fns, spec)
|
|
14
|
+
append_qapplication_delete_ffi_function(fns, spec)
|
|
15
|
+
append_method_ffi_functions(fns, spec)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
fns
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def append_constructor_ffi_function(fns, spec)
|
|
22
|
+
ctor_args = constructor_ffi_args(spec)
|
|
23
|
+
fns << { name: ctor_function_name(spec), ffi_return: :pointer, args: ctor_args }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def constructor_ffi_args(spec)
|
|
27
|
+
return %i[string pointer] if spec[:constructor][:mode] == :keysequence_parent
|
|
28
|
+
return [:pointer] if spec[:constructor][:parent]
|
|
29
|
+
return [:string] if spec[:constructor][:mode] == :string_path
|
|
30
|
+
return [:string] if spec[:constructor][:mode] == :qapplication
|
|
31
|
+
|
|
32
|
+
[]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def append_qapplication_delete_ffi_function(fns, spec)
|
|
36
|
+
return unless spec[:prefix] == 'qapplication'
|
|
37
|
+
|
|
38
|
+
fns << { name: 'qt_ruby_qapplication_delete', ffi_return: :bool, args: [:pointer] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def append_method_ffi_functions(fns, spec)
|
|
42
|
+
spec[:methods].each do |method|
|
|
43
|
+
args = [:pointer] + method[:args].map { |arg| arg[:ffi] }
|
|
44
|
+
fns << { name: method_function_name(spec, method), ffi_return: method[:ffi_return], args: args }
|
|
45
|
+
end
|
|
46
|
+
end
|