lldb 0.1.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 +47 -3
  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 +7977 -856
  10. data/ext/lldb/lldb_wrapper.h +599 -324
  11. data/lib/lldb/address.rb +81 -0
  12. data/lib/lldb/api_support.rb +91 -0
  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 +51 -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 +326 -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 +113 -18
  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 +109 -0
  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 +121 -38
  44. data/lib/lldb/symbol.rb +117 -0
  45. data/lib/lldb/symbol_context.rb +9 -4
  46. data/lib/lldb/target.rb +73 -67
  47. data/lib/lldb/thread.rb +56 -19
  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 +80 -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 +258 -21
  61. data/sig/lldb/weak_ref.rbs +7 -0
  62. metadata +32 -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
@@ -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
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+
6
+ ROOT = File.expand_path('..', __dir__)
7
+ HEADER = File.join(ROOT, 'ext/lldb/lldb_wrapper.h')
8
+ SOURCE = File.join(ROOT, 'ext/lldb/lldb_wrapper.cpp')
9
+ SURFACE = File.join(ROOT, 'bindings/surface.yml')
10
+
11
+ options = { write: false }
12
+ OptionParser.new do |parser|
13
+ parser.banner = 'Usage: script/guard_c_exports [--write]'
14
+ parser.on('--write', 'Rewrite the C ABI exports with exception guards') { options[:write] = true }
15
+ end.parse!
16
+
17
+ def declarations(source)
18
+ source = source.gsub(%r{/\*.*?\*/|//[^\n]*}, '')
19
+ pattern = /(?:^|\n)\s*(?<return_type>[A-Za-z_][A-Za-z0-9_:<>*&\s]*?)\s+(?<name>lldb_[A-Za-z0-9_]+)\s*\((?<arguments>[^;]*)\)\s*(?:LLDB_WRAPPER_NOEXCEPT\s*)?;/m
20
+ source.to_enum(:scan, pattern).map do
21
+ Regexp.last_match.named_captures.transform_values(&:strip)
22
+ end
23
+ end
24
+
25
+ def matching_parenthesis(source, opening)
26
+ depth = 0
27
+ source.byteslice(opening..).each_byte.with_index do |byte, offset|
28
+ depth += 1 if byte == 40
29
+ depth -= 1 if byte == 41
30
+ return opening + offset if depth.zero?
31
+ end
32
+ raise "unclosed function parameter list at #{opening}"
33
+ end
34
+
35
+ def matching_brace(source, opening)
36
+ depth = 0
37
+ source.byteslice(opening..).each_byte.with_index do |byte, offset|
38
+ depth += 1 if byte == 123
39
+ depth -= 1 if byte == 125
40
+ return opening + offset if depth.zero?
41
+ end
42
+ raise "unclosed function body at #{opening}"
43
+ end
44
+
45
+ def definition(source, name)
46
+ pattern = /\b#{Regexp.escape(name)}\s*\(/m
47
+ source.to_enum(:scan, pattern).each do
48
+ opening = Regexp.last_match.begin(0) + Regexp.last_match[0].index('(')
49
+ closing = matching_parenthesis(source, opening)
50
+ after_parameters = source.byteslice((closing + 1)..).to_s
51
+ next unless after_parameters =~ /\A\s*(?:LLDB_WRAPPER_NOEXCEPT\s*)?\{/m
52
+
53
+ body_opening = closing + 1 + Regexp.last_match[0].index('{')
54
+ return [body_opening, matching_brace(source, body_opening)]
55
+ end
56
+ raise "definition not found for #{name}"
57
+ end
58
+
59
+ def guarded_body?(source, opening)
60
+ source.byteslice((opening + 1)..).to_s.match?(/\A\s*try\s*\{/)
61
+ end
62
+
63
+ def catch_block(return_type)
64
+ failure_return = if return_type == 'void'
65
+ ''
66
+ elsif return_type == 'lldb_ruby_status_t'
67
+ ' return LLDB_RUBY_STATUS_INTERNAL_ERROR;' + 10.chr
68
+ else
69
+ ' return {};' + 10.chr
70
+ end
71
+
72
+ <<~CPP
73
+
74
+ } catch (const std::bad_alloc&) {
75
+ wrapper_set_error_state("native allocation failed across the C ABI");
76
+ #{failure_return} } catch (const std::exception& exception) {
77
+ wrapper_set_error_state(exception.what());
78
+ #{failure_return} } catch (...) {
79
+ wrapper_set_error_state("unknown native exception across the C ABI");
80
+ #{failure_return} }
81
+ CPP
82
+ end
83
+
84
+ def rewrite_header(header, entries)
85
+ rewritten = header.dup
86
+ entries.reverse_each do |entry|
87
+ pattern = /\b#{Regexp.escape(entry.fetch('name'))}\s*\([^;]*?\)\s*/m
88
+ match = rewritten.match(pattern)
89
+ raise "declaration not found for #{entry.fetch('name')}" unless match
90
+ next if rewritten.byteslice(match.end(0), 40).to_s.match?(/\A\s*LLDB_WRAPPER_NOEXCEPT\b/)
91
+
92
+ rewritten.insert(match.end(0), ' LLDB_WRAPPER_NOEXCEPT')
93
+ end
94
+ rewritten
95
+ end
96
+
97
+ def rewrite_source(source, entries)
98
+ edits = entries.map do |entry|
99
+ opening, closing = definition(source, entry.fetch('name'))
100
+ next if guarded_body?(source, opening)
101
+
102
+ noexcept = source.byteslice((opening - 80)...opening).to_s.match?(/LLDB_WRAPPER_NOEXCEPT\s*$/)
103
+ [
104
+ [opening, (noexcept ? '' : ' LLDB_WRAPPER_NOEXCEPT')],
105
+ [opening + 1, "\n try {"],
106
+ [closing, catch_block(entry.fetch('return_type'))]
107
+ ]
108
+ end.compact.flatten(1).sort_by(&:first).reverse
109
+
110
+ rewritten = source.dup
111
+ edits.each { |offset, text| rewritten.insert(offset, text) }
112
+ rewritten
113
+ end
114
+
115
+ def rewrite_surface(surface)
116
+ needle = ['kind: reviewed_no_throw', ' reason: The export is tracked explicitly until its native error boundary is upgraded.'].join(10.chr)
117
+ replacement = ['kind: error_boundary', ' reason: Every export is noexcept and catches native C++ exceptions before crossing the C ABI.'].join(10.chr)
118
+
119
+ surface.gsub(needle, replacement)
120
+ end
121
+
122
+ entries = declarations(File.read(HEADER))
123
+ abort "expected 482 declarations, found #{entries.length}" unless entries.length == 482
124
+
125
+ rewritten_header = rewrite_header(File.read(HEADER), entries)
126
+ rewritten_source = rewrite_source(File.read(SOURCE), entries)
127
+ rewritten_surface = rewrite_surface(File.read(SURFACE))
128
+
129
+ if options[:write]
130
+ File.write(HEADER, rewritten_header)
131
+ File.write(SOURCE, rewritten_source)
132
+ File.write(SURFACE, rewritten_surface)
133
+ puts "guarded #{entries.length} C ABI exports"
134
+ else
135
+ abort 'header requires exception-guard updates' unless rewritten_header == File.read(HEADER)
136
+ abort 'source requires exception-guard updates' unless rewritten_source == File.read(SOURCE)
137
+ abort 'surface ledger requires exception-guard updates' unless rewritten_surface == File.read(SURFACE)
138
+ puts "all #{entries.length} C ABI exports are guarded"
139
+ end