Skip to content

Commit

Permalink
Support call stacks on Windows (#11461)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil committed Nov 24, 2021
1 parent 7a1059d commit 90218d9
Show file tree
Hide file tree
Showing 20 changed files with 452 additions and 102 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ jobs:
cl /MT /c src\llvm\ext\llvm_ext.cc -I llvm\include /Fosrc\llvm\ext\llvm_ext.obj
- name: Link Crystal executable
run: |
Invoke-Expression "cl crystal.obj /Fecrystal-cross src\llvm\ext\llvm_ext.obj $(llvm\bin\llvm-config.exe --libs) libs\pcre.lib libs\gc.lib WS2_32.lib advapi32.lib libcmt.lib legacy_stdio_definitions.lib /F10000000"
Invoke-Expression "cl crystal.obj /Fecrystal-cross src\llvm\ext\llvm_ext.obj $(llvm\bin\llvm-config.exe --libs) libs\pcre.lib libs\gc.lib WS2_32.lib advapi32.lib libcmt.lib dbghelp.lib legacy_stdio_definitions.lib /F10000000"
- name: Re-build Crystal
run: |
Expand Down
18 changes: 12 additions & 6 deletions spec/std/exception/call_stack_spec.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "../spec_helper"

describe "Backtrace" do
pending_win32 "prints file line:column" do
it "prints file line:column" do
source_file = datapath("backtrace_sample")

# CallStack tries to make files relative to the current dir,
Expand All @@ -12,17 +12,23 @@ describe "Backtrace" do

_, output, _ = compile_and_run_file(source_file)

# resolved file line:column
output.should match(/^#{source_file}:3:10 in 'callee1'/m)
output.should match(/^#{source_file}:13:5 in 'callee3'/m)
# resolved file:line:column (no column for windows PDB because of poor
# support in general)
{% if flag?(:win32) %}
output.should match(/^#{Regex.escape(source_file)}:3 in 'callee1'/m)
output.should match(/^#{Regex.escape(source_file)}:13 in 'callee3'/m)
{% else %}
output.should match(/^#{Regex.escape(source_file)}:3:10 in 'callee1'/m)
output.should match(/^#{Regex.escape(source_file)}:13:5 in 'callee3'/m)
{% end %}

# skipped internal details
output.should_not contain("src/callstack.cr")
output.should_not contain("src/exception.cr")
output.should_not contain("src/raise.cr")
end

pending_win32 "doesn't relativize paths outside of current dir (#10169)" do
it "doesn't relativize paths outside of current dir (#10169)" do
with_tempfile("source_file") do |source_file|
source_path = Path.new(source_file)
source_path.absolute?.should be_true
Expand All @@ -36,7 +42,7 @@ describe "Backtrace" do
EOF
_, output, _ = compile_and_run_file(source_file)

output.should match /\A(#{source_path}):/
output.should match /\A(#{Regex.escape(source_path.to_s)}):/
end
end

Expand Down
4 changes: 2 additions & 2 deletions spec/std/raise_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ require "./spec_helper"
describe "raise" do
callstack_on_rescue = nil

pending_win32 "should set exception's callstack" do
it "should set exception's callstack" do
exception = expect_raises Exception, "without callstack" do
raise "without callstack"
end
exception.callstack.should_not be_nil
end

pending_win32 "shouldn't overwrite the callstack on re-raise" do
it "shouldn't overwrite the callstack on re-raise" do
exception_after_reraise = expect_raises Exception, "exception to be rescued" do
begin
raise "exception to be rescued"
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/crystal/codegen/codegen.cr
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ module Crystal

def evaluate(node, debug = Debug::Default)
llvm_mod = codegen(node, single_module: true, debug: debug)[""].mod
llvm_mod.target = target_machine.triple

main = llvm_mod.functions[MAIN_NAME]

main_return_type = main.return_type
Expand Down
13 changes: 10 additions & 3 deletions src/compiler/crystal/codegen/debug.cr
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,16 @@ module Crystal
def push_debug_info_metadata(mod)
di_builder(mod).end

# DebugInfo generation in LLVM by default uses a higher version of dwarf
# than OS X currently understands. Android has the same problem.
if @program.has_flag?("osx") || @program.has_flag?("android")
if @program.has_flag?("windows")
# Windows uses CodeView instead of DWARF
mod.add_flag(
LLVM::ModuleFlag::Warning,
"CodeView",
mod.context.int32.const_int(1)
)
elsif @program.has_flag?("osx") || @program.has_flag?("android")
# DebugInfo generation in LLVM by default uses a higher version of dwarf
# than OS X currently understands. Android has the same problem.
mod.add_flag(
LLVM::ModuleFlag::Warning,
"Dwarf Version",
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ module Crystal
object_arg = Process.quote_windows(object_names)
output_arg = Process.quote_windows("/Fe#{output_filename}")

args = %(/nologo #{object_arg} #{output_arg} /link #{lib_flags} #{@link_flags}).gsub("\n", " ")
args = %(/nologo #{object_arg} #{output_arg} /link#{" /DEBUG:FULL" unless debug.none?} #{lib_flags} #{@link_flags}).gsub("\n", " ")
cmd = "#{CL} #{args}"

if cmd.to_utf16.size > 32000
Expand Down
6 changes: 1 addition & 5 deletions src/exception.cr
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,7 @@ class Exception
# The backtrace is an array of strings, each containing
# “0xAddress: Function at File Line Column”.
def backtrace?
{% if flag?(:win32) %}
Array(String).new
{% else %}
@callstack.try &.printable_backtrace
{% end %}
@callstack.try &.printable_backtrace
end

def to_s(io : IO) : Nil
Expand Down
75 changes: 66 additions & 9 deletions src/exception/call_stack.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{% skip_file if flag?(:win32) %}

require "./call_stack/libunwind"
{% if flag?(:win32) %}
require "./call_stack/stackwalk"
{% else %}
require "./call_stack/libunwind"
{% end %}

# Returns the current execution stack as an array containing strings
# usually in the form file:line:column or file:line:column in 'method'.
Expand All @@ -12,12 +14,7 @@ end
struct Exception::CallStack
# Compute current directory at the beginning so filenames
# are always shown relative to the *starting* working directory.
CURRENT_DIR = begin
if dir = Process::INITIAL_PWD
dir += File::SEPARATOR unless dir.ends_with?(File::SEPARATOR)
dir
end
end
private CURRENT_DIR = Process::INITIAL_PWD.try { |dir| Path[dir] }

@@skip = [] of String

Expand All @@ -37,4 +34,64 @@ struct Exception::CallStack
def printable_backtrace : Array(String)
@backtrace ||= decode_backtrace
end

private def decode_backtrace
show_full_info = ENV["CRYSTAL_CALLSTACK_FULL_INFO"]? == "1"

@callstack.compact_map do |ip|
pc = CallStack.decode_address(ip)

file, line_number, column_number = CallStack.decode_line_number(pc)

if file && file != "??"
next if @@skip.includes?(file)

# Turn to relative to the current dir, if possible
if current_dir = CURRENT_DIR
if rel = Path[file].relative_to?(current_dir)
rel = rel.to_s
file = rel unless rel.starts_with?("..")
end
end

file_line_column = file
unless line_number == 0
file_line_column = "#{file_line_column}:#{line_number}"
file_line_column = "#{file_line_column}:#{column_number}" unless column_number == 0
end
end

if name = CallStack.decode_function_name(pc)
function = name
elsif frame = CallStack.decode_frame(ip)
_, function, file = frame
# Crystal methods (their mangled name) start with `*`, so
# we remove that to have less clutter in the output.
function = function.lchop('*')
else
function = "??"
end

if file_line_column
if show_full_info && (frame = CallStack.decode_frame(ip))
_, sname, _ = frame
line = "#{file_line_column} in '#{sname}'"
else
line = "#{file_line_column} in '#{function}'"
end
else
if file == "??" && function == "??"
line = "???"
else
line = "#{file} in '#{function}'"
end
end

if show_full_info
line = "#{line} at 0x#{ip.address.to_s(16)}"
end

line
end
end
end
11 changes: 6 additions & 5 deletions src/exception/call_stack/dwarf.cr
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ struct Exception::CallStack
@@dwarf_function_names : Array(Tuple(LibC::SizeT, LibC::SizeT, String))?

# :nodoc:
def self.load_dwarf
def self.load_debug_info
return if ENV["CRYSTAL_LOAD_DEBUG_INFO"]? == "0"

unless @@dwarf_loaded
@@dwarf_loaded = true
begin
return if ENV["CRYSTAL_LOAD_DWARF"]? == "0"
load_dwarf_impl
load_debug_info_impl
rescue ex
@@dwarf_line_numbers = nil
@@dwarf_function_names = nil
Expand All @@ -26,7 +27,7 @@ struct Exception::CallStack
end

protected def self.decode_line_number(pc)
load_dwarf
load_debug_info
if ln = @@dwarf_line_numbers
if row = ln.find(pc)
return {row.path, row.line, row.column}
Expand All @@ -36,7 +37,7 @@ struct Exception::CallStack
end

protected def self.decode_function_name(pc)
load_dwarf
load_debug_info
if fn = @@dwarf_function_names
fn.each do |(low_pc, high_pc, function_name)|
return function_name if low_pc <= pc <= high_pc
Expand Down
2 changes: 1 addition & 1 deletion src/exception/call_stack/elf.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ require "crystal/elf"
require "c/link"

struct Exception::CallStack
protected def self.load_dwarf_impl
protected def self.load_debug_info_impl
phdr_callback = LibC::DlPhdrCallback.new do |info, size, data|
# The first entry is the header for the current program
read_dwarf_sections(info.value.addr)
Expand Down
53 changes: 0 additions & 53 deletions src/exception/call_stack/libunwind.cr
Original file line number Diff line number Diff line change
Expand Up @@ -129,59 +129,6 @@ struct Exception::CallStack
end
end

private def decode_backtrace
show_full_info = ENV["CRYSTAL_CALLSTACK_FULL_INFO"]? == "1"

@callstack.compact_map do |ip|
pc = CallStack.decode_address(ip)

file, line, column = CallStack.decode_line_number(pc)

if file && file != "??"
next if @@skip.includes?(file)

# Turn to relative to the current dir, if possible
if current_dir = CURRENT_DIR
file = file.lchop(current_dir)
end

file_line_column = "#{file}:#{line}:#{column}"
end

if name = CallStack.decode_function_name(pc)
function = name
elsif frame = CallStack.decode_frame(ip)
_, function, file = frame
# Crystal methods (their mangled name) start with `*`, so
# we remove that to have less clutter in the output.
function = function.lchop('*')
else
function = "??"
end

if file_line_column
if show_full_info && (frame = CallStack.decode_frame(ip))
_, sname, _ = frame
line = "#{file_line_column} in '#{sname}'"
else
line = "#{file_line_column} in '#{function}'"
end
else
if file == "??" && function == "??"
line = "???"
else
line = "#{file} in '#{function}'"
end
end

if show_full_info
line = "#{line} at 0x#{ip.address.to_s(16)}"
end

line
end
end

protected def self.decode_frame(ip, original_ip = ip)
if LibC.dladdr(ip, out info) != 0
offset = original_ip - info.dli_saddr
Expand Down
2 changes: 1 addition & 1 deletion src/exception/call_stack/mach_o.cr
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ end
struct Exception::CallStack
@@image_slide : LibC::Long?

protected def self.load_dwarf_impl
protected def self.load_debug_info_impl
read_dwarf_sections
end

Expand Down
Loading

0 comments on commit 90218d9

Please sign in to comment.