Skip to content

Commit

Permalink
Basic MinGW-w64 cross-compilation support (#15070)
Browse files Browse the repository at this point in the history
Resolves part of #6170. These series of patches allow `--cross-compile --target=x86_64-windows-gnu` to mostly work:

* The `@[ThreadLocal]` annotation and its corresponding LLVM attribute seem to break when targetting `x86_64-windows-gnu`, so Win32 TLS is used instead. This is only needed for `Thread.current`.
* Since MinGW uses `libgcc`, and Crystal relies on the underlying C++ ABI to raise exceptions, we use the Itanium ABI's `_Unwind_*` functions, along with a thin personality function wrapper. ([GCC itself does the same](https://github.com/gcc-mirror/gcc/blob/68afc7acf609be2b19ec05c8393c2ffc7f4adb4a/libgcc/unwind-c.c#L238-L246). See also [Language-specific handler](https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-170#language-specific-handler) from the Windows x64 ABI docs.)
* MinGW binaries now come with DWARF debug information, so they work under GDB, maybe under LLDB, and probably poorly under the Visual Studio Debugger.
* There is no need to mangle symbol names the same way MSVC binaries do.
* `--cross-compile` now prints ld-style linker flags, rather than MSVC ones. This is still incomplete and includes remnants of the MSVC toolchain like the `/ENTRY` flag; they will be fixed later once we get to native compiler builds.
* `src/lib_c/x86_64-windows-gnu` is now a symlink to `src/lib_c/x86_64-windows-msvc`, since both toolchains are targetting the same Win32 APIs. (This is not Cygwin nor MSYS2's MSYS environment.)
* Lib funs now use the Win32 C ABI, instead of the SysV ABI.
* On MinGW we use GMP proper, and there is no need for MPIR.

After building a local compiler, `bin\crystal build --cross-compile --target=x86_64-windows-gnu` will generate an object file suitable for linking under MinGW-w64. At a minimum, this object file depends on Boehm GC and libiconv, although they can be skipped using `-Dgc_none` and `-Dwithout_iconv` respectively. Then we could use MSYS2's UCRT64 environment to link the final executable:

```
$ pacman -Sy mingw-w64-ucrt-x86_64-gc mingw-w64-ucrt-x86_64-pcre2 mingw-w64-ucrt-x86_64-libiconv mingw-w64-ucrt-x86_64-gmp
$ cc test.obj `pkg-config bdw-gc iconv libpcre2-8 gmp --libs` -lDbgHelp -lole32
```

Stack traces do not work correctly yet. Also note that MSYS2's DLL names are different from the ones distributed with MSVC Crystal, and that cross-compilation never copies the DLL dependencies to the output directory. To make the executable run outside MSYS2, use `dumpbin /dependents` from the MSVC developer prompt to obtain the dependencies, then copy them manually from the MSYS2 `/ucrt64/bin` folder.
  • Loading branch information
HertzDevil authored Oct 11, 2024
1 parent 991f9d0 commit d5600eb
Show file tree
Hide file tree
Showing 15 changed files with 173 additions and 52 deletions.
44 changes: 32 additions & 12 deletions spec/std/big/big_float_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,11 @@ describe "BigFloat" do
it { (2.to_big_f ** -7133786264).to_s.should end_with("e-2147483649") } # least power of two with a base-10 exponent less than Int32::MIN
it { (10.to_big_f ** 3000000000 * 1.5).to_s.should end_with("e+3000000000") }
it { (10.to_big_f ** -3000000000 * 1.5).to_s.should end_with("e-3000000000") }
it { (10.to_big_f ** 10000000000 * 1.5).to_s.should end_with("e+10000000000") }
it { (10.to_big_f ** -10000000000 * 1.5).to_s.should end_with("e-10000000000") }

{% unless flag?(:win32) && flag?(:gnu) %}
it { (10.to_big_f ** 10000000000 * 1.5).to_s.should end_with("e+10000000000") }
it { (10.to_big_f ** -10000000000 * 1.5).to_s.should end_with("e-10000000000") }
{% end %}
end

describe "#inspect" do
Expand Down Expand Up @@ -558,42 +561,59 @@ describe "BigFloat Math" do
Math.ilogb(0.2.to_big_f).should eq(-3)
Math.ilogb(123.45.to_big_f).should eq(6)
Math.ilogb(2.to_big_f ** 1_000_000_000).should eq(1_000_000_000)
Math.ilogb(2.to_big_f ** 100_000_000_000).should eq(100_000_000_000)
Math.ilogb(2.to_big_f ** -100_000_000_000).should eq(-100_000_000_000)

{% unless flag?(:win32) && flag?(:gnu) %}
Math.ilogb(2.to_big_f ** 100_000_000_000).should eq(100_000_000_000)
Math.ilogb(2.to_big_f ** -100_000_000_000).should eq(-100_000_000_000)
{% end %}

expect_raises(ArgumentError) { Math.ilogb(0.to_big_f) }
end

it ".logb" do
Math.logb(0.2.to_big_f).should eq(-3.to_big_f)
Math.logb(123.45.to_big_f).should eq(6.to_big_f)
Math.logb(2.to_big_f ** 1_000_000_000).should eq(1_000_000_000.to_big_f)
Math.logb(2.to_big_f ** 100_000_000_000).should eq(100_000_000_000.to_big_f)
Math.logb(2.to_big_f ** -100_000_000_000).should eq(-100_000_000_000.to_big_f)

{% unless flag?(:win32) && flag?(:gnu) %}
Math.logb(2.to_big_f ** 100_000_000_000).should eq(100_000_000_000.to_big_f)
Math.logb(2.to_big_f ** -100_000_000_000).should eq(-100_000_000_000.to_big_f)
{% end %}

expect_raises(ArgumentError) { Math.logb(0.to_big_f) }
end

it ".ldexp" do
Math.ldexp(0.2.to_big_f, 2).should eq(0.8.to_big_f)
Math.ldexp(0.2.to_big_f, -2).should eq(0.05.to_big_f)
Math.ldexp(1.to_big_f, 1_000_000_000).should eq(2.to_big_f ** 1_000_000_000)
Math.ldexp(1.to_big_f, 100_000_000_000).should eq(2.to_big_f ** 100_000_000_000)
Math.ldexp(1.to_big_f, -100_000_000_000).should eq(0.5.to_big_f ** 100_000_000_000)

{% unless flag?(:win32) && flag?(:gnu) %}
Math.ldexp(1.to_big_f, 100_000_000_000).should eq(2.to_big_f ** 100_000_000_000)
Math.ldexp(1.to_big_f, -100_000_000_000).should eq(0.5.to_big_f ** 100_000_000_000)
{% end %}
end

it ".scalbn" do
Math.scalbn(0.2.to_big_f, 2).should eq(0.8.to_big_f)
Math.scalbn(0.2.to_big_f, -2).should eq(0.05.to_big_f)
Math.scalbn(1.to_big_f, 1_000_000_000).should eq(2.to_big_f ** 1_000_000_000)
Math.scalbn(1.to_big_f, 100_000_000_000).should eq(2.to_big_f ** 100_000_000_000)
Math.scalbn(1.to_big_f, -100_000_000_000).should eq(0.5.to_big_f ** 100_000_000_000)

{% unless flag?(:win32) && flag?(:gnu) %}
Math.scalbn(1.to_big_f, 100_000_000_000).should eq(2.to_big_f ** 100_000_000_000)
Math.scalbn(1.to_big_f, -100_000_000_000).should eq(0.5.to_big_f ** 100_000_000_000)
{% end %}
end

it ".scalbln" do
Math.scalbln(0.2.to_big_f, 2).should eq(0.8.to_big_f)
Math.scalbln(0.2.to_big_f, -2).should eq(0.05.to_big_f)
Math.scalbln(1.to_big_f, 1_000_000_000).should eq(2.to_big_f ** 1_000_000_000)
Math.scalbln(1.to_big_f, 100_000_000_000).should eq(2.to_big_f ** 100_000_000_000)
Math.scalbln(1.to_big_f, -100_000_000_000).should eq(0.5.to_big_f ** 100_000_000_000)

{% unless flag?(:win32) && flag?(:gnu) %}
Math.scalbln(1.to_big_f, 100_000_000_000).should eq(2.to_big_f ** 100_000_000_000)
Math.scalbln(1.to_big_f, -100_000_000_000).should eq(0.5.to_big_f ** 100_000_000_000)
{% end %}
end

it ".frexp" do
Expand Down
4 changes: 2 additions & 2 deletions src/big/big_int.cr
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ struct BigInt < Int
{% for n in [8, 16, 32, 64, 128] %}
def to_i{{n}} : Int{{n}}
\{% if Int{{n}} == LibGMP::SI %}
LibGMP.{{ flag?(:win32) ? "fits_si_p".id : "fits_slong_p".id }}(self) != 0 ? LibGMP.get_si(self) : raise OverflowError.new
LibGMP.{{ flag?(:win32) && !flag?(:gnu) ? "fits_si_p".id : "fits_slong_p".id }}(self) != 0 ? LibGMP.get_si(self) : raise OverflowError.new
\{% elsif Int{{n}}::MAX.is_a?(NumberLiteral) && Int{{n}}::MAX < LibGMP::SI::MAX %}
LibGMP::SI.new(self).to_i{{n}}
\{% else %}
Expand All @@ -669,7 +669,7 @@ struct BigInt < Int

def to_u{{n}} : UInt{{n}}
\{% if UInt{{n}} == LibGMP::UI %}
LibGMP.{{ flag?(:win32) ? "fits_ui_p".id : "fits_ulong_p".id }}(self) != 0 ? LibGMP.get_ui(self) : raise OverflowError.new
LibGMP.{{ flag?(:win32) && !flag?(:gnu) ? "fits_ui_p".id : "fits_ulong_p".id }}(self) != 0 ? LibGMP.get_ui(self) : raise OverflowError.new
\{% elsif UInt{{n}}::MAX.is_a?(NumberLiteral) && UInt{{n}}::MAX < LibGMP::UI::MAX %}
LibGMP::UI.new(self).to_u{{n}}
\{% else %}
Expand Down
29 changes: 16 additions & 13 deletions src/big/lib_gmp.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% if flag?(:win32) %}
{% if flag?(:win32) && !flag?(:gnu) %}
@[Link("mpir")]
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
@[Link(dll: "mpir.dll")]
Expand All @@ -14,7 +14,7 @@ lib LibGMP
# MPIR uses its own `mpir_si` and `mpir_ui` typedefs in places where GMP uses
# the LibC types, when the function name has `si` or `ui`; we follow this
# distinction
{% if flag?(:win32) && flag?(:bits64) %}
{% if flag?(:win32) && !flag?(:gnu) && flag?(:bits64) %}
alias SI = LibC::LongLong
alias UI = LibC::ULongLong
{% else %}
Expand All @@ -26,17 +26,19 @@ lib LibGMP
alias Double = LibC::Double
alias BitcntT = UI

{% if flag?(:win32) && flag?(:bits64) %}
alias MpExp = LibC::Long
alias MpExp = LibC::Long

{% if flag?(:win32) && !flag?(:gnu) %}
alias MpSize = LibC::LongLong
alias MpLimb = LibC::ULongLong
{% elsif flag?(:bits64) %}
alias MpExp = Int64
alias MpSize = LibC::Long
alias MpLimb = LibC::ULong
{% else %}
alias MpExp = Int32
alias MpSize = LibC::Long
{% end %}

# NOTE: this assumes GMP is configured by build time to define
# `_LONG_LONG_LIMB=1` on Windows
{% if flag?(:win32) %}
alias MpLimb = LibC::ULongLong
{% else %}
alias MpLimb = LibC::ULong
{% end %}

Expand Down Expand Up @@ -149,11 +151,12 @@ lib LibGMP

# # Miscellaneous Functions

fun fits_ulong_p = __gmpz_fits_ulong_p(op : MPZ*) : Int
fun fits_slong_p = __gmpz_fits_slong_p(op : MPZ*) : Int
{% if flag?(:win32) %}
{% if flag?(:win32) && !flag?(:gnu) %}
fun fits_ui_p = __gmpz_fits_ui_p(op : MPZ*) : Int
fun fits_si_p = __gmpz_fits_si_p(op : MPZ*) : Int
{% else %}
fun fits_ulong_p = __gmpz_fits_ulong_p(op : MPZ*) : Int
fun fits_slong_p = __gmpz_fits_slong_p(op : MPZ*) : Int
{% end %}

# # Special Functions
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/crystal/codegen/codegen.cr
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ module Crystal
@main = @llvm_mod.functions.add(MAIN_NAME, main_type)
@fun_types = { {@llvm_mod, MAIN_NAME} => main_type }

if @program.has_flag? "windows"
if @program.has_flag?("msvc")
@personality_name = "__CxxFrameHandler3"
@main.personality_function = windows_personality_fun.func
else
Expand Down Expand Up @@ -2488,7 +2488,7 @@ module Crystal
end

def self.safe_mangling(program, name)
if program.has_flag?("windows")
if program.has_flag?("msvc")
String.build do |str|
name.each_char do |char|
if char.ascii_alphanumeric? || char == '_'
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/codegen/debug.cr
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ module Crystal
def push_debug_info_metadata(mod)
di_builder(mod).end

if @program.has_flag?("windows")
if @program.has_flag?("msvc")
# Windows uses CodeView instead of DWARF
mod.add_flag(
LibLLVM::ModuleFlagBehavior::Warning,
Expand Down
17 changes: 9 additions & 8 deletions src/compiler/crystal/codegen/exception.cr
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ class Crystal::CodeGenVisitor
#
# Note we codegen the ensure body three times! In practice this isn't a big deal, since ensure bodies are typically small.

windows = @program.has_flag? "windows"
msvc = @program.has_flag?("msvc")

context.fun.personality_function = windows_personality_fun.func if windows
context.fun.personality_function = windows_personality_fun.func if msvc

# This is the block which is entered when the body raises an exception
rescue_block = new_block "rescue"
Expand Down Expand Up @@ -109,7 +109,7 @@ class Crystal::CodeGenVisitor

old_catch_pad = @catch_pad

if windows
if msvc
# Windows structured exception handling must enter a catch_switch instruction
# which decides which catch body block to enter. Crystal only ever generates one catch body
# which is used for all exceptions. For more information on how structured exception handling works in LLVM,
Expand Down Expand Up @@ -138,7 +138,8 @@ class Crystal::CodeGenVisitor
caught_exception = load exception_llvm_type, caught_exception_ptr
exception_type_id = type_id(caught_exception, exception_type)
else
# Unwind exception handling code - used on non-windows platforms - is a lot simpler.
# Unwind exception handling code - used on non-MSVC platforms (essentially the Itanium
# C++ ABI) - is a lot simpler.
# First we generate the landing pad instruction, this returns a tuple of the libunwind
# exception object and the type ID of the exception. This tuple is set up in the crystal
# personality function in raise.cr
Expand Down Expand Up @@ -188,7 +189,7 @@ class Crystal::CodeGenVisitor
# If the rescue restriction matches, codegen the rescue block.
position_at_end this_rescue_block

# On windows, we are "inside" the catchpad block. It's difficult to track when to catch_ret when
# On MSVC, we are "inside" the catchpad block. It's difficult to track when to catch_ret when
# codegenning the entire rescue body, so we catch_ret early and execute the rescue bodies "outside" the
# rescue block.
if catch_pad = @catch_pad
Expand Down Expand Up @@ -248,7 +249,7 @@ class Crystal::CodeGenVisitor

# Codegen catchswitch+pad or landing pad as described above.
# This code is simpler because we never need to extract the exception type
if windows
if msvc
rescue_ensure_body = new_block "rescue_ensure_body"
catch_switch = builder.catch_switch(old_catch_pad || LLVM::Value.null, @rescue_block || LLVM::BasicBlock.null, 1)
builder.add_handler catch_switch, rescue_ensure_body
Expand Down Expand Up @@ -283,8 +284,8 @@ class Crystal::CodeGenVisitor
end

def codegen_re_raise(node, unwind_ex_obj)
if @program.has_flag? "windows"
# On windows we can re-raise by calling _CxxThrowException with two null arguments
if @program.has_flag?("msvc")
# On the MSVC C++ ABI we can re-raise by calling _CxxThrowException with two null arguments
call windows_throw_fun, [llvm_context.void_pointer.null, llvm_context.void_pointer.null]
unreachable
else
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/codegen/link.cr
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ module Crystal

class Program
def lib_flags
has_flag?("windows") ? lib_flags_windows : lib_flags_posix
has_flag?("msvc") ? lib_flags_windows : lib_flags_posix
end

private def lib_flags_windows
Expand Down
47 changes: 42 additions & 5 deletions src/crystal/system/win32/thread.cr
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,49 @@ module Crystal::System::Thread
LibC.SwitchToThread
end

@[ThreadLocal]
class_property current_thread : ::Thread { ::Thread.new }
# MinGW does not support TLS correctly
{% if flag?(:gnu) %}
@@current_key : LibC::DWORD = begin
current_key = LibC.TlsAlloc
if current_key == LibC::TLS_OUT_OF_INDEXES
Crystal::System.panic("TlsAlloc()", WinError.value)
end
current_key
end

def self.current_thread? : ::Thread?
@@current_thread
end
def self.current_thread : ::Thread
th = current_thread?
return th if th

# Thread#start sets `Thread.current` as soon it starts. Thus we know
# that if `Thread.current` is not set then we are in the main thread
self.current_thread = ::Thread.new
end

def self.current_thread? : ::Thread?
ptr = LibC.TlsGetValue(@@current_key)
err = WinError.value
unless err == WinError::ERROR_SUCCESS
Crystal::System.panic("TlsGetValue()", err)
end

ptr.as(::Thread?)
end

def self.current_thread=(thread : ::Thread)
if LibC.TlsSetValue(@@current_key, thread.as(Void*)) == 0
Crystal::System.panic("TlsSetValue()", WinError.value)
end
thread
end
{% else %}
@[ThreadLocal]
class_property current_thread : ::Thread { ::Thread.new }

def self.current_thread? : ::Thread?
@@current_thread
end
{% end %}

def self.sleep(time : ::Time::Span) : Nil
LibC.Sleep(time.total_milliseconds.to_i.clamp(1..))
Expand Down
3 changes: 3 additions & 0 deletions src/exception/call_stack.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
require "./call_stack/interpreter"
{% elsif flag?(:win32) %}
require "./call_stack/stackwalk"
{% if flag?(:gnu) %}
require "./lib_unwind"
{% end %}
{% elsif flag?(:wasm32) %}
require "./call_stack/null"
{% else %}
Expand Down
27 changes: 27 additions & 0 deletions src/exception/call_stack/stackwalk.cr
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,33 @@ struct Exception::CallStack
end
end

# TODO: needed only if `__crystal_raise` fails, check if this actually works
{% if flag?(:gnu) %}
def self.print_backtrace : Nil
backtrace_fn = ->(context : LibUnwind::Context, data : Void*) do
last_frame = data.as(RepeatedFrame*)

ip = {% if flag?(:arm) %}
Pointer(Void).new(__crystal_unwind_get_ip(context))
{% else %}
Pointer(Void).new(LibUnwind.get_ip(context))
{% end %}

if last_frame.value.ip == ip
last_frame.value.incr
else
print_frame(last_frame.value) unless last_frame.value.ip.address == 0
last_frame.value = RepeatedFrame.new ip
end
LibUnwind::ReasonCode::NO_REASON
end

rf = RepeatedFrame.new(Pointer(Void).null)
LibUnwind.backtrace(backtrace_fn, pointerof(rf).as(Void*))
print_frame(rf)
end
{% end %}

private def self.print_frame(repeated_frame)
Crystal::System.print_error "[%p] ", repeated_frame.ip
print_frame_location(repeated_frame)
Expand Down
8 changes: 6 additions & 2 deletions src/exception/lib_unwind.cr
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,12 @@ lib LibUnwind
struct Exception
exception_class : LibC::SizeT
exception_cleanup : LibC::SizeT
private1 : UInt64
private2 : UInt64
{% if flag?(:win32) && flag?(:gnu) %}
private_ : UInt64[6]
{% else %}
private1 : UInt64
private2 : UInt64
{% end %}
exception_object : Void*
exception_type_id : Int32
end
Expand Down
1 change: 1 addition & 0 deletions src/lib_c/x86_64-windows-gnu
6 changes: 6 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,11 @@ lib LibC
fun ResumeThread(hThread : HANDLE) : DWORD
fun SuspendThread(hThread : HANDLE) : DWORD

TLS_OUT_OF_INDEXES = 0xFFFFFFFF_u32

fun TlsAlloc : DWORD
fun TlsGetValue(dwTlsIndex : DWORD) : Void*
fun TlsSetValue(dwTlsIndex : DWORD, lpTlsValue : Void*) : BOOL

PROCESS_QUERY_INFORMATION = 0x0400
end
2 changes: 1 addition & 1 deletion src/llvm/target_machine.cr
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class LLVM::TargetMachine
def abi
triple = self.triple
case triple
when /x86_64.+windows-msvc/
when /x86_64.+windows-(?:msvc|gnu)/
ABI::X86_Win64.new(self)
when /x86_64|amd64/
ABI::X86_64.new(self)
Expand Down
Loading

0 comments on commit d5600eb

Please sign in to comment.