jolt-ruby 1.0.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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitmodules +3 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +228 -0
  5. data/Rakefile +180 -0
  6. data/examples/character_virtual.rb +35 -0
  7. data/examples/falling_sphere.rb +25 -0
  8. data/examples/stagecraft_binding.rb +29 -0
  9. data/ext/jolt_ruby/CMakeLists.txt +42 -0
  10. data/ext/jolt_ruby/character_helper.cpp +44 -0
  11. data/ext/jolt_ruby/constraint_helper.cpp +154 -0
  12. data/ext/jolt_ruby/extconf.rb +56 -0
  13. data/ext/jolt_ruby/helper.cpp +225 -0
  14. data/ext/jolt_ruby/helper.h +103 -0
  15. data/ext/joltc/.github/FUNDING.yml +1 -0
  16. data/ext/joltc/.github/workflows/build.yml +298 -0
  17. data/ext/joltc/.gitignore +272 -0
  18. data/ext/joltc/CMakeLists.txt +431 -0
  19. data/ext/joltc/LICENSE +21 -0
  20. data/ext/joltc/README.md +10 -0
  21. data/ext/joltc/build/cmake_vs2026_arm64.bat +3 -0
  22. data/ext/joltc/build/cmake_vs2026_clang.bat +10 -0
  23. data/ext/joltc/build/cmake_vs2026_x64.bat +3 -0
  24. data/ext/joltc/build/cmake_vs2026_x64_double.bat +3 -0
  25. data/ext/joltc/include/joltc.h +3166 -0
  26. data/ext/joltc/samples/01_HelloWorld/main.cpp +170 -0
  27. data/ext/joltc/samples/CMakeLists.txt +35 -0
  28. data/ext/joltc/src/joltc.c +4 -0
  29. data/ext/joltc/src/joltc.cpp +11812 -0
  30. data/ext/joltc/src/joltc_assert.cpp +271 -0
  31. data/ext/joltc/tests/CMakeLists.txt +77 -0
  32. data/ext/joltc/tests/test_character.cpp +149 -0
  33. data/ext/joltc/tests/test_collision.cpp +146 -0
  34. data/ext/joltc/tests/test_constraints.cpp +353 -0
  35. data/ext/joltc/tests/test_core.cpp +94 -0
  36. data/ext/joltc/tests/test_math.cpp +585 -0
  37. data/ext/joltc/tests/test_physics_system.cpp +465 -0
  38. data/ext/joltc/tests/test_shapes.cpp +789 -0
  39. data/ext/joltc/tests/test_skeleton.cpp +370 -0
  40. data/ext/joltc/tests/test_vehicle.cpp +319 -0
  41. data/generator/generate.rb +237 -0
  42. data/generator/layout_probe.cpp +489 -0
  43. data/generator/verify_layout.rb +32 -0
  44. data/lib/jolt/body.rb +147 -0
  45. data/lib/jolt/body_collection.rb +115 -0
  46. data/lib/jolt/body_dynamics.rb +93 -0
  47. data/lib/jolt/character_virtual.rb +162 -0
  48. data/lib/jolt/constraint.rb +136 -0
  49. data/lib/jolt/constraint_collection.rb +123 -0
  50. data/lib/jolt/contact_events.rb +19 -0
  51. data/lib/jolt/conversions.rb +76 -0
  52. data/lib/jolt/errors.rb +12 -0
  53. data/lib/jolt/fixed_stepper.rb +37 -0
  54. data/lib/jolt/hit.rb +5 -0
  55. data/lib/jolt/layers.rb +182 -0
  56. data/lib/jolt/native/character_functions.rb +16 -0
  57. data/lib/jolt/native/constraint_functions.rb +27 -0
  58. data/lib/jolt/native/core_functions.rb +17 -0
  59. data/lib/jolt/native/generated.rb +1995 -0
  60. data/lib/jolt/native/platform.rb +51 -0
  61. data/lib/jolt/native/query_functions.rb +43 -0
  62. data/lib/jolt/native/types.rb +28 -0
  63. data/lib/jolt/native.rb +94 -0
  64. data/lib/jolt/shape.rb +90 -0
  65. data/lib/jolt/shape_builders.rb +155 -0
  66. data/lib/jolt/system.rb +238 -0
  67. data/lib/jolt/system_characters.rb +25 -0
  68. data/lib/jolt/system_constraints.rb +36 -0
  69. data/lib/jolt/system_contacts.rb +57 -0
  70. data/lib/jolt/system_queries.rb +111 -0
  71. data/lib/jolt/transform.rb +5 -0
  72. data/lib/jolt/version.rb +5 -0
  73. data/lib/jolt.rb +83 -0
  74. data/script/smoke_gem.rb +29 -0
  75. data/script/verify_release.rb +36 -0
  76. metadata +147 -0
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi/clang"
4
+
5
+ class BindingGenerator
6
+ ROOT = File.expand_path("..", __dir__)
7
+ HEADER = File.join(ROOT, "ext", "joltc", "include", "joltc.h")
8
+ RUBY_OUTPUT = File.join(ROOT, "lib", "jolt", "native", "generated.rb")
9
+ PROBE_OUTPUT = File.join(__dir__, "layout_probe.cpp")
10
+
11
+ INTEGER_TYPES = {
12
+ type_char_s: :int8,
13
+ type_schar: :int8,
14
+ type_uchar: :uint8,
15
+ type_short: :int16,
16
+ type_ushort: :uint16,
17
+ type_int: :int32,
18
+ type_uint: :uint32,
19
+ type_long: :long,
20
+ type_ulong: :ulong,
21
+ type_longlong: :int64,
22
+ type_ulonglong: :uint64
23
+ }.freeze
24
+
25
+ def initialize
26
+ @translation_unit = FFI::Clang::Index.new.parse_translation_unit(
27
+ HEADER, %w[-x c++ -std=c++17]
28
+ )
29
+ errors = @translation_unit.diagnostics.select { |diagnostic| diagnostic.severity >= 3 }
30
+ abort errors.map(&:spelling).join("\n") unless errors.empty?
31
+ end
32
+
33
+ def run(check: false)
34
+ outputs = {
35
+ RUBY_OUTPUT => render_ruby,
36
+ PROBE_OUTPUT => render_probe
37
+ }
38
+ if check
39
+ stale = outputs.filter_map { |path, content| path unless File.binread(path) == content }
40
+ abort "generated bindings are stale: #{stale.join(", ")}" unless stale.empty?
41
+ else
42
+ outputs.each { |path, content| File.binwrite(path, content) }
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def declarations(kind)
49
+ @translation_unit.cursor.each.filter_map do |cursor, _parent|
50
+ cursor if cursor.kind == kind && cursor.location.file.to_s == HEADER
51
+ end
52
+ end
53
+
54
+ def records
55
+ declarations(:cursor_struct)
56
+ .select { |cursor| cursor.definition? && !cursor.spelling.empty? }
57
+ .uniq(&:spelling)
58
+ end
59
+
60
+ def functions
61
+ declarations(:cursor_function).uniq(&:spelling)
62
+ end
63
+
64
+ def fields(record)
65
+ record.each(false).filter_map do |cursor, _parent|
66
+ cursor if cursor.kind == :cursor_field_decl
67
+ end
68
+ end
69
+
70
+ def sorted_records
71
+ remaining = records.to_h { |record| [record.spelling, record] }
72
+ sorted = []
73
+ until remaining.empty?
74
+ ready = remaining.values.select do |record|
75
+ record_dependencies(record).none? { |name| remaining.key?(name) }
76
+ end
77
+ abort "cyclic struct dependency: #{remaining.keys.join(", ")}" if ready.empty?
78
+
79
+ ready.sort_by(&:spelling).each do |record|
80
+ sorted << record
81
+ remaining.delete(record.spelling)
82
+ end
83
+ end
84
+ sorted
85
+ end
86
+
87
+ def record_dependencies(record)
88
+ fields(record).filter_map do |field|
89
+ type = field.type.canonical
90
+ type = type.element_type.canonical if type.kind == :type_constant_array
91
+ type.spelling if type.kind == :type_record && type.spelling != record.spelling
92
+ end.uniq
93
+ end
94
+
95
+ def render_ruby
96
+ lines = [
97
+ "# frozen_string_literal: true",
98
+ "",
99
+ "# Generated from ext/joltc/include/joltc.h by generator/generate.rb.",
100
+ "# Do not edit this file directly.",
101
+ "",
102
+ "require \"ffi\"",
103
+ "",
104
+ "module Jolt",
105
+ " module Native",
106
+ " module Generated"
107
+ ]
108
+ sorted_records.each { |record| render_record(lines, record) }
109
+ render_function_table(lines)
110
+ render_layout_table(lines)
111
+ lines.concat [
112
+ "",
113
+ " BLOCKING_FUNCTIONS = %i[JPH_PhysicsSystem_Update].freeze",
114
+ "",
115
+ " module_function",
116
+ "",
117
+ " def attach(native, names: FUNCTIONS.keys)",
118
+ " missing = []",
119
+ " names.each do |name|",
120
+ " arguments, result = FUNCTIONS.fetch(name)",
121
+ " options = BLOCKING_FUNCTIONS.include?(name) ? { blocking: true } : {}",
122
+ " begin",
123
+ " native.attach_function(name, arguments, result, **options)",
124
+ " rescue FFI::NotFoundError",
125
+ " missing << name",
126
+ " end",
127
+ " end",
128
+ " @missing_functions = missing.freeze",
129
+ " end",
130
+ "",
131
+ " def missing_functions",
132
+ " @missing_functions || [].freeze",
133
+ " end",
134
+ " end",
135
+ " end",
136
+ "end",
137
+ ""
138
+ ]
139
+ lines.join("\n")
140
+ end
141
+
142
+ def render_record(lines, record)
143
+ lines << ""
144
+ lines << " class #{ruby_class_name(record.spelling)} < FFI::Struct"
145
+ layout = fields(record).flat_map do |field|
146
+ [":#{snake_case(field.spelling)}", ruby_type(field.type, field: true)]
147
+ end
148
+ if layout.empty?
149
+ lines << " end"
150
+ return
151
+ end
152
+
153
+ lines << " layout #{layout.each_slice(2).map { |pair| pair.join(", ") }.join(",\n ")}"
154
+ lines << " end"
155
+ end
156
+
157
+ def render_function_table(lines)
158
+ lines << ""
159
+ lines << " FUNCTIONS = {"
160
+ functions.sort_by(&:spelling).each do |function|
161
+ arguments = function.num_arguments.times.map do |index|
162
+ ruby_type(function.argument(index).type, argument: true)
163
+ end
164
+ result = ruby_type(function.result_type)
165
+ lines << " #{function.spelling}: [[#{arguments.join(", ")}], #{result}],"
166
+ end
167
+ lines << " }.freeze"
168
+ end
169
+
170
+ def render_layout_table(lines)
171
+ lines << ""
172
+ lines << " HEADER_LAYOUTS = {"
173
+ sorted_records.each do |record|
174
+ offsets = fields(record).map do |field|
175
+ "#{snake_case(field.spelling)}: #{field.offset_of_field / 8}"
176
+ end
177
+ lines << " #{record.spelling.inspect} => [#{record.type.sizeof}, { #{offsets.join(", ")} }],"
178
+ end
179
+ lines << " }.freeze"
180
+ end
181
+
182
+ def render_probe
183
+ lines = [
184
+ "// Generated from ext/joltc/include/joltc.h by generator/generate.rb.",
185
+ "// Do not edit this file directly.",
186
+ "#include <cstddef>",
187
+ "#include <cstdio>",
188
+ "#include <joltc.h>",
189
+ "",
190
+ "int main() {"
191
+ ]
192
+ sorted_records.each do |record|
193
+ lines << %( std::printf("S\\t#{record.spelling}\\t%zu\\n", sizeof(#{record.spelling}));)
194
+ fields(record).each do |field|
195
+ lines << %( std::printf("F\\t#{record.spelling}\\t#{snake_case(field.spelling)}\\t%zu\\n", offsetof(#{record.spelling}, #{field.spelling}));)
196
+ end
197
+ end
198
+ lines.concat [" return 0;", "}", ""]
199
+ lines.join("\n")
200
+ end
201
+
202
+ def ruby_type(type, field: false, argument: false)
203
+ canonical = type.canonical
204
+ kind = canonical.kind
205
+ return ":pointer" if argument && kind == :type_constant_array
206
+ return ":pointer" if kind == :type_pointer
207
+ return ":void" if kind == :type_void
208
+ return ":bool" if kind == :type_bool
209
+ return ":float" if kind == :type_float
210
+ return ":double" if kind == :type_double
211
+ return ":int" if kind == :type_enum
212
+ return INTEGER_TYPES.fetch(kind).inspect if INTEGER_TYPES.key?(kind)
213
+ if kind == :type_record
214
+ suffix = field || argument ? ".by_value" : ".by_value"
215
+ return "#{ruby_class_name(canonical.spelling)}#{suffix}"
216
+ end
217
+ if kind == :type_constant_array
218
+ return "[#{ruby_type(canonical.element_type, field: true)}, #{canonical.size}]"
219
+ end
220
+
221
+ abort "unsupported C type: #{type.spelling} (#{kind})"
222
+ end
223
+
224
+ def ruby_class_name(c_name)
225
+ c_name.delete_prefix("JPH_")
226
+ end
227
+
228
+ def snake_case(name)
229
+ name
230
+ .sub(/\A_+/, "")
231
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
232
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
233
+ .downcase
234
+ end
235
+ end
236
+
237
+ BindingGenerator.new.run(check: ARGV.delete("--check"))