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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +27 -0
  3. data/README.md +303 -0
  4. data/Rakefile +94 -0
  5. data/examples/development_ordered_demos/01_dsl_hello.rb +22 -0
  6. data/examples/development_ordered_demos/02_live_layout_console.rb +137 -0
  7. data/examples/development_ordered_demos/03_component_showcase.rb +235 -0
  8. data/examples/development_ordered_demos/04_paint_simple.rb +147 -0
  9. data/examples/development_ordered_demos/05_tetris_simple.rb +295 -0
  10. data/examples/development_ordered_demos/06_timetrap_clockify.rb +759 -0
  11. data/examples/development_ordered_demos/07_peek_like_recorder.rb +597 -0
  12. data/examples/qtproject/widgets/itemviews/spreadsheet/main.rb +252 -0
  13. data/examples/qtproject/widgets/widgetsgallery/main.rb +184 -0
  14. data/ext/qt_ruby_bridge/extconf.rb +75 -0
  15. data/ext/qt_ruby_bridge/qt_ruby_runtime.hpp +23 -0
  16. data/ext/qt_ruby_bridge/runtime_events.cpp +408 -0
  17. data/ext/qt_ruby_bridge/runtime_signals.cpp +212 -0
  18. data/lib/qt/application_lifecycle.rb +44 -0
  19. data/lib/qt/bridge.rb +95 -0
  20. data/lib/qt/children_tracking.rb +15 -0
  21. data/lib/qt/constants.rb +10 -0
  22. data/lib/qt/date_time_codec.rb +104 -0
  23. data/lib/qt/errors.rb +6 -0
  24. data/lib/qt/event_runtime.rb +139 -0
  25. data/lib/qt/event_runtime_dispatch.rb +35 -0
  26. data/lib/qt/event_runtime_qobject_methods.rb +41 -0
  27. data/lib/qt/generated_constants_runtime.rb +33 -0
  28. data/lib/qt/inspectable.rb +29 -0
  29. data/lib/qt/key_sequence_codec.rb +22 -0
  30. data/lib/qt/native.rb +93 -0
  31. data/lib/qt/shortcut_compat.rb +30 -0
  32. data/lib/qt/string_codec.rb +44 -0
  33. data/lib/qt/variant_codec.rb +78 -0
  34. data/lib/qt/version.rb +5 -0
  35. data/lib/qt.rb +47 -0
  36. data/scripts/generate_bridge/ast_introspection.rb +267 -0
  37. data/scripts/generate_bridge/auto_method_spec_resolver.rb +37 -0
  38. data/scripts/generate_bridge/auto_methods.rb +438 -0
  39. data/scripts/generate_bridge/core_utils.rb +114 -0
  40. data/scripts/generate_bridge/cpp_method_return_emitter.rb +93 -0
  41. data/scripts/generate_bridge/ffi_api.rb +46 -0
  42. data/scripts/generate_bridge/free_function_specs.rb +289 -0
  43. data/scripts/generate_bridge/spec_discovery.rb +313 -0
  44. data/scripts/generate_bridge.rb +1113 -0
  45. 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