lldb 0.2.0 → 0.3.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -4
  3. data/README.md +86 -2
  4. data/Rakefile +36 -0
  5. data/bindings/constants.yml +137 -0
  6. data/bindings/surface.yml +3072 -0
  7. data/ext/lldb/discovery.rb +101 -0
  8. data/ext/lldb/extconf.rb +206 -88
  9. data/ext/lldb/lldb_wrapper.cpp +7934 -877
  10. data/ext/lldb/lldb_wrapper.h +596 -333
  11. data/lib/lldb/address.rb +81 -0
  12. data/lib/lldb/api_support.rb +23 -6
  13. data/lib/lldb/attach_info.rb +122 -0
  14. data/lib/lldb/block.rb +121 -0
  15. data/lib/lldb/breakpoint.rb +11 -6
  16. data/lib/lldb/breakpoint_location.rb +20 -5
  17. data/lib/lldb/broadcaster.rb +79 -0
  18. data/lib/lldb/command_interpreter.rb +9 -4
  19. data/lib/lldb/command_return_object.rb +24 -5
  20. data/lib/lldb/compile_unit.rb +64 -0
  21. data/lib/lldb/context.rb +81 -0
  22. data/lib/lldb/debugger.rb +55 -12
  23. data/lib/lldb/error.rb +50 -3
  24. data/lib/lldb/event.rb +109 -0
  25. data/lib/lldb/expression_options.rb +124 -0
  26. data/lib/lldb/ffi_bindings.rb +313 -31
  27. data/lib/lldb/file_spec.rb +90 -0
  28. data/lib/lldb/file_spec_list.rb +78 -0
  29. data/lib/lldb/frame.rb +109 -17
  30. data/lib/lldb/function.rb +121 -0
  31. data/lib/lldb/instruction.rb +83 -0
  32. data/lib/lldb/instruction_list.rb +68 -0
  33. data/lib/lldb/launch_info.rb +145 -19
  34. data/lib/lldb/line_entry.rb +75 -0
  35. data/lib/lldb/listener.rb +96 -0
  36. data/lib/lldb/memory_region_info.rb +8 -3
  37. data/lib/lldb/module.rb +46 -5
  38. data/lib/lldb/native.rb +36 -0
  39. data/lib/lldb/native_buffer.rb +32 -0
  40. data/lib/lldb/native_handle.rb +60 -0
  41. data/lib/lldb/native_lifecycle.rb +64 -0
  42. data/lib/lldb/native_string_array.rb +34 -0
  43. data/lib/lldb/process.rb +102 -40
  44. data/lib/lldb/symbol.rb +117 -0
  45. data/lib/lldb/symbol_context.rb +9 -4
  46. data/lib/lldb/target.rb +69 -67
  47. data/lib/lldb/thread.rb +51 -18
  48. data/lib/lldb/type.rb +40 -3
  49. data/lib/lldb/type_member.rb +67 -0
  50. data/lib/lldb/types.rb +69 -4
  51. data/lib/lldb/value.rb +27 -17
  52. data/lib/lldb/value_list.rb +9 -5
  53. data/lib/lldb/version.rb +1 -1
  54. data/lib/lldb/watchpoint.rb +26 -9
  55. data/lib/lldb.rb +78 -10
  56. data/lldb.gemspec +4 -1
  57. data/rbs_collection.yaml +1 -1
  58. data/script/check_bindings +259 -0
  59. data/script/guard_c_exports +139 -0
  60. data/sig/lldb/ffi_bindings.rbs +247 -21
  61. data/sig/lldb/weak_ref.rbs +7 -0
  62. metadata +30 -6
  63. data/ext/lldb/Makefile +0 -24
  64. data/ext/lldb/liblldb_wrapper.dylib +0 -0
  65. data/ext/lldb/lldb_wrapper.o +0 -0
  66. data/ext/lldb/mkmf.log +0 -24
data/lib/lldb/value.rb CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  module LLDB
6
6
  class Value
7
+ prepend NativeLifecycle
7
8
  include Enumerable #[Value]
8
9
 
9
10
  # @rbs return: Frame | Value | Target
@@ -12,10 +13,13 @@ module LLDB
12
13
  # @rbs ptr: FFI::Pointer
13
14
  # @rbs parent: Frame | Value | Target
14
15
  # @rbs return: void
15
- def initialize(ptr, parent:)
16
- @ptr = ptr # : FFI::Pointer
16
+ def initialize(ptr, parent:, context: parent.context)
17
17
  @parent = parent
18
- ObjectSpace.define_finalizer(self, self.class.release(@ptr))
18
+ initialize_native_object(
19
+ ptr,
20
+ release: ->(released) { FFIBindings.lldb_value_destroy(released) },
21
+ context: context
22
+ )
19
23
  end
20
24
 
21
25
  # @rbs ptr: FFI::Pointer
@@ -72,7 +76,7 @@ module LLDB
72
76
  child_ptr = FFIBindings.lldb_value_get_child_at_index(@ptr, index)
73
77
  return nil if child_ptr.nil? || child_ptr.null?
74
78
 
75
- Value.new(child_ptr, parent: self)
79
+ Value.new(child_ptr, parent: self, context: context)
76
80
  end
77
81
 
78
82
  # @rbs name: String
@@ -83,7 +87,7 @@ module LLDB
83
87
  child_ptr = FFIBindings.lldb_value_get_child_member_with_name(@ptr, name)
84
88
  return nil if child_ptr.nil? || child_ptr.null?
85
89
 
86
- Value.new(child_ptr, parent: self)
90
+ Value.new(child_ptr, parent: self, context: context)
87
91
  end
88
92
 
89
93
  alias [] child_member
@@ -105,7 +109,10 @@ module LLDB
105
109
  def value_as_signed
106
110
  return 0 unless valid?
107
111
 
108
- FFIBindings.lldb_value_get_value_as_signed(@ptr)
112
+ error = Error.new
113
+ result = FFIBindings.lldb_value_get_value_as_signed(@ptr, error.to_ptr, 0)
114
+ error.raise_if_error!('value.value_as_signed')
115
+ result
109
116
  end
110
117
 
111
118
  alias to_i value_as_signed
@@ -114,7 +121,10 @@ module LLDB
114
121
  def value_as_unsigned
115
122
  return 0 unless valid?
116
123
 
117
- FFIBindings.lldb_value_get_value_as_unsigned(@ptr)
124
+ error = Error.new
125
+ result = FFIBindings.lldb_value_get_value_as_unsigned(@ptr, error.to_ptr, 0)
126
+ error.raise_if_error!('value.value_as_unsigned')
127
+ result
118
128
  end
119
129
 
120
130
  # @rbs return: Integer
@@ -153,7 +163,7 @@ module LLDB
153
163
  deref_ptr = FFIBindings.lldb_value_dereference(@ptr)
154
164
  return nil if deref_ptr.nil? || deref_ptr.null?
155
165
 
156
- Value.new(deref_ptr, parent: self)
166
+ Value.new(deref_ptr, parent: self, context: context)
157
167
  end
158
168
 
159
169
  # @rbs return: Value?
@@ -163,7 +173,7 @@ module LLDB
163
173
  addr_ptr = FFIBindings.lldb_value_address_of(@ptr)
164
174
  return nil if addr_ptr.nil? || addr_ptr.null?
165
175
 
166
- Value.new(addr_ptr, parent: self)
176
+ Value.new(addr_ptr, parent: self, context: context)
167
177
  end
168
178
 
169
179
  # @rbs return: String
@@ -189,7 +199,7 @@ module LLDB
189
199
  type_ptr = FFIBindings.lldb_value_get_type(@ptr)
190
200
  return nil if type_ptr.nil? || type_ptr.null?
191
201
 
192
- Type.new(type_ptr)
202
+ Type.new(type_ptr, context: context)
193
203
  end
194
204
 
195
205
  # @rbs target_type: Type
@@ -200,12 +210,12 @@ module LLDB
200
210
  cast_ptr = FFIBindings.lldb_value_cast(@ptr, target_type.to_ptr)
201
211
  return nil if cast_ptr.nil? || cast_ptr.null?
202
212
 
203
- Value.new(cast_ptr, parent: self)
213
+ Value.new(cast_ptr, parent: self, context: context)
204
214
  end
205
215
 
206
216
  # @rbs return: Integer
207
217
  def load_address
208
- return 0 unless valid?
218
+ return INVALID_ADDRESS unless valid?
209
219
 
210
220
  FFIBindings.lldb_value_get_load_address(@ptr)
211
221
  end
@@ -243,7 +253,7 @@ module LLDB
243
253
  child_ptr = FFIBindings.lldb_value_create_child_at_offset(@ptr, name, value_type.to_ptr, offset)
244
254
  return nil if child_ptr.nil? || child_ptr.null?
245
255
 
246
- Value.new(child_ptr, parent: self)
256
+ Value.new(child_ptr, parent: self, context: context)
247
257
  end
248
258
 
249
259
  # @rbs name: String
@@ -256,7 +266,7 @@ module LLDB
256
266
  value_ptr = FFIBindings.lldb_value_create_value_from_address(@ptr, name, address, value_type.to_ptr)
257
267
  return nil if value_ptr.nil? || value_ptr.null?
258
268
 
259
- Value.new(value_ptr, parent: self)
269
+ Value.new(value_ptr, parent: self, context: context)
260
270
  end
261
271
 
262
272
  # @rbs name: String
@@ -268,7 +278,7 @@ module LLDB
268
278
  value_ptr = FFIBindings.lldb_value_create_value_from_expression(@ptr, name, expression)
269
279
  return nil if value_ptr.nil? || value_ptr.null?
270
280
 
271
- Value.new(value_ptr, parent: self)
281
+ Value.new(value_ptr, parent: self, context: context)
272
282
  end
273
283
 
274
284
  # @rbs resolve: bool
@@ -285,7 +295,7 @@ module LLDB
285
295
  return nil if wp_ptr.nil? || wp_ptr.null?
286
296
 
287
297
  target = get_target_from_parent
288
- Watchpoint.new(wp_ptr, target: target)
298
+ Watchpoint.new(wp_ptr, target: target, context: context)
289
299
  end
290
300
 
291
301
  # @rbs return: String?
@@ -309,7 +319,7 @@ module LLDB
309
319
  value_ptr = FFIBindings.lldb_value_get_non_synthetic_value(@ptr)
310
320
  return nil if value_ptr.nil? || value_ptr.null?
311
321
 
312
- Value.new(value_ptr, parent: self)
322
+ Value.new(value_ptr, parent: self, context: context)
313
323
  end
314
324
 
315
325
  # @rbs return: FFI::Pointer
@@ -4,6 +4,7 @@
4
4
 
5
5
  module LLDB
6
6
  class ValueList
7
+ prepend NativeLifecycle
7
8
  include Enumerable #[Value]
8
9
 
9
10
  # @rbs return: Frame | Value
@@ -12,10 +13,13 @@ module LLDB
12
13
  # @rbs ptr: FFI::Pointer
13
14
  # @rbs parent: Frame | Value
14
15
  # @rbs return: void
15
- def initialize(ptr, parent:)
16
- @ptr = ptr # : FFI::Pointer
16
+ def initialize(ptr, parent:, context: parent.context)
17
17
  @parent = parent
18
- ObjectSpace.define_finalizer(self, self.class.release(@ptr))
18
+ initialize_native_object(
19
+ ptr,
20
+ release: ->(released) { FFIBindings.lldb_value_list_destroy(released) },
21
+ context: context
22
+ )
19
23
  end
20
24
 
21
25
  # @rbs ptr: FFI::Pointer
@@ -46,7 +50,7 @@ module LLDB
46
50
  value_ptr = FFIBindings.lldb_value_list_get_value_at_index(@ptr, index)
47
51
  return nil if value_ptr.nil? || value_ptr.null?
48
52
 
49
- Value.new(value_ptr, parent: @parent)
53
+ Value.new(value_ptr, parent: @parent, context: context)
50
54
  end
51
55
 
52
56
  alias [] value_at_index
@@ -59,7 +63,7 @@ module LLDB
59
63
  value_ptr = FFIBindings.lldb_value_list_get_first_value_by_name(@ptr, name)
60
64
  return nil if value_ptr.nil? || value_ptr.null?
61
65
 
62
- Value.new(value_ptr, parent: @parent)
66
+ Value.new(value_ptr, parent: @parent, context: context)
63
67
  end
64
68
 
65
69
  # @rbs return: Array[Value]
data/lib/lldb/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  module LLDB
6
- VERSION = '0.2.0' # : String
6
+ VERSION = '0.3.0' # : String
7
7
  end
@@ -4,16 +4,21 @@
4
4
 
5
5
  module LLDB
6
6
  class Watchpoint
7
+ prepend NativeLifecycle
8
+
7
9
  # @rbs return: Target
8
10
  attr_reader :target
9
11
 
10
12
  # @rbs ptr: FFI::Pointer
11
13
  # @rbs target: Target
12
14
  # @rbs return: void
13
- def initialize(ptr, target:)
14
- @ptr = ptr # : FFI::Pointer
15
+ def initialize(ptr, target:, context: target.context)
15
16
  @target = target
16
- ObjectSpace.define_finalizer(self, self.class.release(@ptr))
17
+ initialize_native_object(
18
+ ptr,
19
+ release: ->(released) { FFIBindings.lldb_watchpoint_destroy(released) },
20
+ context: context
21
+ )
17
22
  end
18
23
 
19
24
  # @rbs ptr: FFI::Pointer
@@ -29,7 +34,7 @@ module LLDB
29
34
 
30
35
  # @rbs return: Integer
31
36
  def id
32
- return -1 unless valid?
37
+ return INVALID_BREAK_ID unless valid?
33
38
 
34
39
  FFIBindings.lldb_watchpoint_get_id(@ptr)
35
40
  end
@@ -99,7 +104,7 @@ module LLDB
99
104
 
100
105
  # @rbs return: Integer
101
106
  def watch_address
102
- return 0 unless valid?
107
+ return INVALID_ADDRESS unless valid?
103
108
 
104
109
  FFIBindings.lldb_watchpoint_get_watch_address(@ptr)
105
110
  end
@@ -113,16 +118,28 @@ module LLDB
113
118
 
114
119
  # @rbs return: bool
115
120
  def watching_reads?
116
- return false unless valid?
121
+ raise InvalidObjectError, 'Watchpoint is not valid' unless valid?
122
+ APISupport.require_feature!(:watchpoint_access_kind)
117
123
 
118
- FFIBindings.lldb_watchpoint_is_watching_reads(@ptr) != 0
124
+ result = FFI::MemoryPointer.new(:int)
125
+ status = FFIBindings.lldb_watchpoint_is_watching_reads(@ptr, result)
126
+ raise UnsupportedAPIError, 'Watchpoint read access is unsupported' if status == NativeStatus::UNSUPPORTED
127
+ raise LLDBError, "Failed to query watchpoint read access (status=#{status})" unless status == NativeStatus::OK
128
+
129
+ result.read_int != 0
119
130
  end
120
131
 
121
132
  # @rbs return: bool
122
133
  def watching_writes?
123
- return false unless valid?
134
+ raise InvalidObjectError, 'Watchpoint is not valid' unless valid?
135
+ APISupport.require_feature!(:watchpoint_access_kind)
136
+
137
+ result = FFI::MemoryPointer.new(:int)
138
+ status = FFIBindings.lldb_watchpoint_is_watching_writes(@ptr, result)
139
+ raise UnsupportedAPIError, 'Watchpoint write access is unsupported' if status == NativeStatus::UNSUPPORTED
140
+ raise LLDBError, "Failed to query watchpoint write access (status=#{status})" unless status == NativeStatus::OK
124
141
 
125
- FFIBindings.lldb_watchpoint_is_watching_writes(@ptr) != 0
142
+ result.read_int != 0
126
143
  end
127
144
 
128
145
  # @rbs return: bool
data/lib/lldb.rb CHANGED
@@ -3,10 +3,25 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  require_relative 'lldb/version'
6
+ require_relative 'lldb/error'
6
7
  require_relative 'lldb/ffi_bindings'
7
8
  require_relative 'lldb/types'
8
- require_relative 'lldb/error'
9
+ require_relative 'lldb/native'
10
+ require_relative 'lldb/native_handle'
11
+ require_relative 'lldb/context'
12
+ require_relative 'lldb/native_lifecycle'
13
+ require_relative 'lldb/native_buffer'
9
14
  require_relative 'lldb/api_support'
15
+ require_relative 'lldb/native_string_array'
16
+ require_relative 'lldb/file_spec'
17
+ require_relative 'lldb/file_spec_list'
18
+ require_relative 'lldb/address'
19
+ require_relative 'lldb/line_entry'
20
+ require_relative 'lldb/attach_info'
21
+ require_relative 'lldb/expression_options'
22
+ require_relative 'lldb/broadcaster'
23
+ require_relative 'lldb/listener'
24
+ require_relative 'lldb/event'
10
25
  require_relative 'lldb/debugger'
11
26
  require_relative 'lldb/target'
12
27
  require_relative 'lldb/launch_info'
@@ -19,6 +34,13 @@ require_relative 'lldb/breakpoint_location'
19
34
  require_relative 'lldb/value'
20
35
  require_relative 'lldb/value_list'
21
36
  require_relative 'lldb/type'
37
+ require_relative 'lldb/type_member'
38
+ require_relative 'lldb/symbol'
39
+ require_relative 'lldb/function'
40
+ require_relative 'lldb/compile_unit'
41
+ require_relative 'lldb/block'
42
+ require_relative 'lldb/instruction'
43
+ require_relative 'lldb/instruction_list'
22
44
  require_relative 'lldb/watchpoint'
23
45
  require_relative 'lldb/module'
24
46
  require_relative 'lldb/symbol_context'
@@ -29,20 +51,29 @@ module LLDB
29
51
  class << self
30
52
  # @rbs return: void
31
53
  def initialize
32
- return if @initialized
54
+ lifecycle_mutex.synchronize do
55
+ return if @initialized
33
56
 
34
- FFIBindings.lldb_initialize
35
- @initialized = true
57
+ error = Error.new
58
+ status = FFIBindings.lldb_initialize(error.to_ptr)
59
+ Native.check_status!(status, 'lldb.initialize', error)
60
+ @initialized = true
36
61
 
37
- at_exit { terminate }
62
+ at_exit { terminate if open_debugger_count.zero? }
63
+ end
38
64
  end
39
65
 
40
66
  # @rbs return: void
41
67
  def terminate
42
- return unless @initialized
68
+ lifecycle_mutex.synchronize do
69
+ return unless @initialized
70
+ if live_debugger_count.positive?
71
+ raise LifecycleError, 'cannot terminate LLDB while debuggers are open'
72
+ end
43
73
 
44
- FFIBindings.lldb_terminate
45
- @initialized = false
74
+ FFIBindings.lldb_terminate
75
+ @initialized = false
76
+ end
46
77
  end
47
78
 
48
79
  # @rbs return: bool
@@ -58,9 +89,46 @@ module LLDB
58
89
  end
59
90
 
60
91
  # @rbs return: Debugger
61
- def create_debugger
92
+ def create_debugger(source_init_files: false)
62
93
  LLDB.initialize
63
- Debugger.create
94
+ Debugger.create(source_init_files: source_init_files)
95
+ end
96
+
97
+ # @rbs return: Integer
98
+ def open_debugger_count
99
+ lifecycle_mutex.synchronize { live_debugger_count }
100
+ end
101
+
102
+ # @rbs debugger: Debugger
103
+ # @rbs return: void
104
+ def register_debugger(debugger)
105
+ lifecycle_mutex.synchronize do
106
+ debuggers = (@debuggers ||= []) # : Array[WeakRef]
107
+ debuggers << WeakRef.new(debugger)
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ # @rbs return: Integer
114
+ def live_debugger_count
115
+ live = [] # : Array[WeakRef]
116
+ count = (@debuggers || []).count do |reference|
117
+ begin
118
+ debugger = reference.__getobj__
119
+ live << reference
120
+ !debugger.closed?
121
+ rescue WeakRef::RefError
122
+ false
123
+ end
124
+ end
125
+ @debuggers = live
126
+ count
127
+ end
128
+
129
+ # @rbs return: Mutex
130
+ def lifecycle_mutex
131
+ @lifecycle_mutex ||= Mutex.new
64
132
  end
65
133
  end
66
134
  end
data/lldb.gemspec CHANGED
@@ -25,7 +25,10 @@ Gem::Specification.new do |spec|
25
25
  end
26
26
  end
27
27
 
28
- spec.files += Dir['lib/**/*.rb', 'ext/**/*']
28
+ generated_extension_file = %r{\Aext/lldb/(?:Makefile|mkmf\.log|lldb_wrapper_config\.h|.*\.(?:o|so|dylib|dll))\z}
29
+ spec.files += Dir['lib/**/*.rb', 'ext/**/*'].select do |file|
30
+ File.file?(file) && !file.match?(generated_extension_file)
31
+ end
29
32
  spec.extensions = ['ext/lldb/extconf.rb']
30
33
  spec.require_paths = ['lib']
31
34
 
data/rbs_collection.yaml CHANGED
@@ -3,7 +3,7 @@ sources:
3
3
  - type: git
4
4
  name: ruby/gem_rbs_collection
5
5
  remote: https://github.com/ruby/gem_rbs_collection.git
6
- revision: main
6
+ revision: b01c0409f55afb1c03e43077c61c44bae61026eb
7
7
  repo_dir: gems
8
8
 
9
9
  # You can specify local directories as sources also.
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'open3'
6
+ require 'optparse'
7
+ require 'rbconfig'
8
+ require 'shellwords'
9
+ require 'tempfile'
10
+ require 'tmpdir'
11
+ require 'yaml'
12
+
13
+ ROOT = File.expand_path('..', __dir__)
14
+
15
+ options = {
16
+ header: File.join(ROOT, 'ext/lldb/lldb_wrapper.h'),
17
+ source: File.join(ROOT, 'ext/lldb/lldb_wrapper.cpp'),
18
+ ffi: File.join(ROOT, 'lib/lldb/ffi_bindings.rb'),
19
+ rbs: File.join(ROOT, 'sig/lldb/ffi_bindings.rbs'),
20
+ surface: File.join(ROOT, 'bindings/surface.yml'),
21
+ constants: File.join(ROOT, 'bindings/constants.yml'),
22
+ library: nil,
23
+ include: ENV['LLDB_INCLUDE'],
24
+ skip_library: false,
25
+ skip_constants: false
26
+ }
27
+
28
+ OptionParser.new do |parser|
29
+ parser.banner = 'Usage: script/check_bindings [options]'
30
+ parser.on('--header PATH') { |value| options[:header] = value }
31
+ parser.on('--source PATH') { |value| options[:source] = value }
32
+ parser.on('--ffi PATH') { |value| options[:ffi] = value }
33
+ parser.on('--rbs PATH') { |value| options[:rbs] = value }
34
+ parser.on('--surface PATH') { |value| options[:surface] = value }
35
+ parser.on('--constants PATH') { |value| options[:constants] = value }
36
+ parser.on('--library PATH') { |value| options[:library] = value }
37
+ parser.on('--include PATH') { |value| options[:include] = value }
38
+ parser.on('--skip-library') { options[:skip_library] = true }
39
+ parser.on('--skip-constants') { options[:skip_constants] = true }
40
+ end.parse!
41
+
42
+ def read_required(path)
43
+ abort "missing binding input: #{path}" unless File.file?(path)
44
+
45
+ File.read(path)
46
+ end
47
+
48
+ def header_functions(source)
49
+ source = source.gsub(%r{/\*.*?\*/|//[^\n]*}, '')
50
+ source.scan(/(?:^|\n)\s*(?:[A-Za-z_][A-Za-z0-9_:<>*&\s]*?)\s+(lldb_[A-Za-z0-9_]+)\s*\([^;]*\)\s*(?:LLDB_WRAPPER_NOEXCEPT\s*)?;/m).flatten.uniq.sort
51
+ end
52
+
53
+ def ffi_functions(source)
54
+ source.scan(/\battach_function\s+:([a-z0-9_]+)/).flatten.uniq.sort
55
+ end
56
+
57
+ def rbs_functions(source)
58
+ source.scan(/\bdef self\.(lldb_[a-z0-9_]+):/).flatten.uniq.sort
59
+ end
60
+
61
+ def exported_functions(path)
62
+ command = if RbConfig::CONFIG['host_os'].match?(/darwin/)
63
+ ['nm', '-gU', path]
64
+ else
65
+ ['nm', '-D', '--defined-only', path]
66
+ end
67
+ stdout, stderr, status = Open3.capture3(*command)
68
+ abort "cannot inspect native library #{path}: #{stderr.strip}" unless status.success?
69
+
70
+ stdout.lines.filter_map do |line|
71
+ name = line.split.last
72
+ next unless name&.match?(/\A_?lldb_[A-Za-z0-9_]+\z/)
73
+
74
+ name.delete_prefix('_').to_sym.to_s
75
+ end.uniq.sort
76
+ end
77
+
78
+ def report_difference(label, left_name, left, right_name, right)
79
+ missing = left - right
80
+ extra = right - left
81
+ return [] if missing.empty? && extra.empty?
82
+
83
+ messages = []
84
+ messages << "#{label}: missing from #{right_name}: #{missing.join(', ')}" unless missing.empty?
85
+ messages << "#{label}: missing from #{left_name}: #{extra.join(', ')}" unless extra.empty?
86
+ messages
87
+ end
88
+
89
+ def matching_parenthesis(source, opening)
90
+ depth = 0
91
+ source.byteslice(opening..).each_byte.with_index do |byte, offset|
92
+ depth += 1 if byte == 40
93
+ depth -= 1 if byte == 41
94
+ return opening + offset if depth.zero?
95
+ end
96
+ nil
97
+ end
98
+
99
+ def function_body(source, name)
100
+ opening = nil
101
+ source.to_enum(:scan, /\b#{Regexp.escape(name)}\s*\(/m).each do
102
+ match = Regexp.last_match
103
+ opening_parenthesis = match.begin(0) + match[0].index('(')
104
+ closing_parenthesis = matching_parenthesis(source, opening_parenthesis)
105
+ next unless closing_parenthesis
106
+
107
+ suffix = source.byteslice((closing_parenthesis + 1)..).to_s
108
+ opening_offset = suffix.index(/\A\s*(?:LLDB_WRAPPER_NOEXCEPT\s*)?\{/m)
109
+ next unless opening_offset
110
+
111
+ opening = closing_parenthesis + 1 + opening_offset + suffix[opening_offset..].index('{')
112
+ break
113
+ end
114
+ return nil unless opening
115
+
116
+ depth = 0
117
+ source.byteslice(opening..).each_byte.with_index do |byte, offset|
118
+ depth += 1 if byte == 123
119
+ depth -= 1 if byte == 125
120
+ return source.byteslice(opening, offset + 1) if depth.zero?
121
+ end
122
+ nil
123
+ end
124
+
125
+ def check_surface(path, functions, source)
126
+ document = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
127
+ entries = document.fetch('entries')
128
+ errors = []
129
+
130
+ errors << "surface ledger has missing entries: #{(functions - entries.keys).sort.join(', ')}" unless
131
+ (functions - entries.keys).empty?
132
+ errors << "surface ledger has stale entries: #{(entries.keys - functions).sort.join(', ')}" unless
133
+ (entries.keys - functions).empty?
134
+
135
+ allowed_classifications = %w[public internal metadata optional_stub deprecated]
136
+ entries.each do |name, entry|
137
+ next unless functions.include?(name)
138
+
139
+ classification = entry.fetch('classification', '')
140
+ unless allowed_classifications.include?(classification)
141
+ errors << "surface ledger entry #{name} has invalid classification #{classification.inspect}"
142
+ end
143
+ if entry.fetch('reason', '').to_s.strip.empty?
144
+ errors << "surface ledger entry #{name} has no reason"
145
+ end
146
+
147
+ guard = entry.fetch('exception_guard', {})
148
+ kind = guard.fetch('kind', '')
149
+ reason = guard.fetch('reason', '').to_s.strip
150
+ if reason.empty?
151
+ errors << "surface ledger entry #{name} has no exception guard reason"
152
+ elsif kind == 'error_boundary'
153
+ body = function_body(source, name)
154
+ unless body&.include?('try') && body.include?('catch')
155
+ errors << "#{name} is marked error_boundary but has no try/catch body"
156
+ end
157
+ unless source.match?(/\b#{Regexp.escape(name)}\s*\([^;]*\)\s*LLDB_WRAPPER_NOEXCEPT\s*\{/m)
158
+ errors << "#{name} is marked error_boundary but is not declared noexcept"
159
+ end
160
+ elsif kind != 'noexcept'
161
+ errors << "surface ledger entry #{name} has invalid exception guard #{kind.inspect}"
162
+ end
163
+ end
164
+
165
+ methods = document.fetch('ruby_methods', [])
166
+ methods.each do |mapping|
167
+ file = File.join(ROOT, mapping.fetch('file'))
168
+ method = mapping.fetch('method')
169
+ unless File.file?(file) && File.read(file).match?(/^\s*def #{Regexp.escape(method)}\b/)
170
+ errors << "#{mapping.fetch('function')} maps to missing Ruby method #{mapping.fetch('method')} in #{file}"
171
+ end
172
+ end
173
+ errors
174
+ end
175
+
176
+ def find_include_dir(explicit)
177
+ return explicit if explicit && File.file?(File.join(explicit, 'lldb/API/LLDB.h'))
178
+
179
+ discovery = File.join(ROOT, 'ext/lldb/discovery.rb')
180
+ require discovery
181
+ LLDB::BuildDiscovery.candidates.find do |candidate|
182
+ File.file?(File.join(candidate.include_dir.to_s, 'lldb/API/LLDB.h'))
183
+ end&.include_dir
184
+ end
185
+
186
+ def check_constants(path, include_dir)
187
+ document = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
188
+ constants = document.fetch('constants')
189
+ return [] if constants.empty?
190
+
191
+ unless include_dir
192
+ return ['cannot check constants: LLDB include directory was not found']
193
+ end
194
+
195
+ source = <<~CPP
196
+ #include <lldb/API/LLDB.h>
197
+ #include <iostream>
198
+ int main() {
199
+ #{constants.map { |entry| " std::cout << static_cast<unsigned long long>(#{entry.fetch('native')}) << '\\n';" }.join("\n")}
200
+ return 0;
201
+ }
202
+ CPP
203
+
204
+ Dir.mktmpdir('lldb-ruby-constants') do |directory|
205
+ source_path = File.join(directory, 'probe.cpp')
206
+ binary_path = File.join(directory, 'probe')
207
+ File.write(source_path, source)
208
+ compiler = Shellwords.split(ENV.fetch('CXX', RbConfig::CONFIG['CXX'] || 'c++'))
209
+ _stdout, stderr, status = Open3.capture3(
210
+ *compiler, '-std=c++17', '-I', include_dir, source_path, '-o', binary_path
211
+ )
212
+ return ["constant probe compilation failed: #{stderr.strip}"] unless status.success?
213
+
214
+ native_output, native_error, native_status = Open3.capture3(binary_path)
215
+ return ["constant probe failed: #{native_error.strip}"] unless native_status.success?
216
+
217
+ ruby_code = constants.map { |entry| "puts LLDB::#{entry.fetch('ruby')}" }.join('; ')
218
+ ruby_output, ruby_error, ruby_status = Open3.capture3(
219
+ RbConfig.ruby, '-I', File.join(ROOT, 'lib'), '-r', 'lldb/types', '-e', ruby_code
220
+ )
221
+ return ["Ruby constant probe failed: #{ruby_error.strip}"] unless ruby_status.success?
222
+
223
+ native_values = native_output.lines.map(&:strip)
224
+ ruby_values = ruby_output.lines.map(&:strip)
225
+ errors = []
226
+ constants.each_with_index do |entry, index|
227
+ next if native_values[index] == ruby_values[index]
228
+
229
+ errors << "constant #{entry.fetch('ruby')} differs: Ruby=#{ruby_values[index].inspect}, " \
230
+ "LLDB=#{native_values[index].inspect}"
231
+ end
232
+ errors
233
+ end
234
+ end
235
+
236
+ header = header_functions(read_required(options[:header]))
237
+ ffi = ffi_functions(read_required(options[:ffi]))
238
+ rbs = rbs_functions(read_required(options[:rbs]))
239
+ source = read_required(options[:source])
240
+ errors = []
241
+ errors.concat(report_difference('header/FFI', 'header', header, 'FFI', ffi))
242
+ errors.concat(report_difference('FFI/RBS', 'FFI', ffi, 'RBS', rbs))
243
+
244
+ unless options[:skip_library]
245
+ library = options[:library] || Dir[File.join(ROOT, 'lib/lldb/liblldb_wrapper.{so,dylib,dll}')].first
246
+ errors << 'native library was not found; pass --library or --skip-library' unless library
247
+ errors.concat(report_difference('header/native exports', 'header', header, 'native exports', exported_functions(library))) if library
248
+ end
249
+
250
+ errors.concat(check_surface(options[:surface], header, source))
251
+ errors.concat(check_constants(options[:constants], find_include_dir(options[:include]))) unless options[:skip_constants]
252
+
253
+ if errors.empty?
254
+ puts "binding parity OK (#{header.length} functions)"
255
+ exit 0
256
+ end
257
+
258
+ warn errors.map { |error| "error: #{error}" }.join("\n")
259
+ exit 1