From 9d8c2e4a1a7521949324a30aa6774495620fa5f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gustavo=20Gir=C3=A1ldez?= <gustavo.giraldez@gmail.com>
Date: Wed, 23 Oct 2024 04:38:09 -0400
Subject: [PATCH 1/7] Inline `ASTNode` bindings dependencies and observers
 (#15098)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Every `ASTNode` contains two arrays used for type inference and checking: dependencies and observers. By default, these are created lazily, but most active (ie. effectively typed) `ASTNode`s end up creating them. Furthermore, on average both these lists contain less than 2 elements each.

This PR replaces both `Array(ASTNode)?` references in `ASTNode` by inlined structs that can hold two elements and a tail array for the cases where more links are needed. This reduces the number of allocations, bytes allocated, number of instructions executed and running time.

Some numbers from compiling the Crystal compiler itself without running codegen (since type binding occurs in the semantic phase anyway).

* Running time (measured with hyperfine running with `GC_DONT_GC=1`): ~6% reduction
  Before:
  ```
  Benchmark 1: ./self-semantic-only.sh
    Time (mean ± σ):      3.398 s ±  0.152 s    [User: 2.264 s, System: 0.470 s]
    Range (min … max):    3.029 s …  3.575 s    10 runs
  ```

  After:
  ```
  Benchmark 1: ./self-semantic-only.sh
    Time (mean ± σ):      3.180 s ±  0.095 s    [User: 2.153 s, System: 0.445 s]
    Range (min … max):    3.046 s …  3.345 s    10 runs
  ```

* Memory (as reported by the compiler itself, with GC): ~9.6% reduction
  Before:
  ```
  Parse:                             00:00:00.000038590 (   1.05MB)
  Semantic (top level):              00:00:00.483357706 ( 174.13MB)
  Semantic (new):                    00:00:00.002156811 ( 174.13MB)
  Semantic (type declarations):      00:00:00.038313066 ( 174.13MB)
  Semantic (abstract def check):     00:00:00.014283169 ( 190.13MB)
  Semantic (restrictions augmenter): 00:00:00.010672651 ( 206.13MB)
  Semantic (ivars initializers):     00:00:04.660611385 (1250.07MB)
  Semantic (cvars initializers):     00:00:00.008343907 (1250.07MB)
  Semantic (main):                   00:00:00.780627942 (1346.07MB)
  Semantic (cleanup):                00:00:00.000961914 (1346.07MB)
  Semantic (recursive struct check): 00:00:00.001121766 (1346.07MB)
  ```

  After:
  ```
  Parse:                             00:00:00.000044417 (   1.05MB)
  Semantic (top level):              00:00:00.546445955 ( 190.03MB)
  Semantic (new):                    00:00:00.002488975 ( 190.03MB)
  Semantic (type declarations):      00:00:00.040234541 ( 206.03MB)
  Semantic (abstract def check):     00:00:00.015473723 ( 222.03MB)
  Semantic (restrictions augmenter): 00:00:00.010828366 ( 222.03MB)
  Semantic (ivars initializers):     00:00:03.324639987 (1135.96MB)
  Semantic (cvars initializers):     00:00:00.007359853 (1135.96MB)
  Semantic (main):                   00:00:01.806822202 (1217.96MB)
  Semantic (cleanup):                00:00:00.000626975 (1217.96MB)
  Semantic (recursive struct check): 00:00:00.001435494 (1217.96MB)
  ```

* Callgrind stats:
  - Instruction refs: 17,477,865,704 -> 16,712,610,033 (~4.4% reduction)
  - Estimated cycles: 26,835,733,874 -> 26,154,926,143 (~2.5% reduction)
  - `GC_malloc_kind` call count: 35,161,616 -> 25,684,997 (~27% reduction)

Co-authored-by: Oleh Prypin <oleh@pryp.in>
Co-authored-by: Johannes Müller <straightshoota@gmail.com>
---
 src/compiler/crystal/semantic/bindings.cr     | 111 ++++++++++++++----
 src/compiler/crystal/semantic/call_error.cr   |   3 +-
 .../crystal/semantic/cleanup_transformer.cr   |   5 +-
 src/compiler/crystal/semantic/filters.cr      |   2 +-
 src/compiler/crystal/semantic/main_visitor.cr |   4 +-
 src/compiler/crystal/semantic/type_merge.cr   |  10 +-
 6 files changed, 97 insertions(+), 38 deletions(-)

diff --git a/src/compiler/crystal/semantic/bindings.cr b/src/compiler/crystal/semantic/bindings.cr
index c5fe9f164742..a7dacb8668c9 100644
--- a/src/compiler/crystal/semantic/bindings.cr
+++ b/src/compiler/crystal/semantic/bindings.cr
@@ -1,7 +1,77 @@
 module Crystal
+  # Specialized container for ASTNodes to use for bindings tracking.
+  #
+  # The average number of elements in both dependencies and observers is below 2
+  # for ASTNodes. This struct inlines the first two elements saving up 4
+  # allocations per node (two arrays, with a header and buffer for each) but we
+  # need to pay a slight extra cost in memory upfront: a total of 6 pointers (48
+  # bytes) vs 2 pointers (16 bytes). The other downside is that since this is a
+  # struct, we need to be careful with mutation.
+  struct SmallNodeList
+    include Enumerable(ASTNode)
+
+    @first : ASTNode?
+    @second : ASTNode?
+    @tail : Array(ASTNode)?
+
+    def each(& : ASTNode ->)
+      yield @first || return
+      yield @second || return
+      @tail.try(&.each { |node| yield node })
+    end
+
+    def size
+      if @first.nil?
+        0
+      elsif @second.nil?
+        1
+      elsif (tail = @tail).nil?
+        2
+      else
+        2 + tail.size
+      end
+    end
+
+    def push(node : ASTNode) : self
+      if @first.nil?
+        @first = node
+      elsif @second.nil?
+        @second = node
+      elsif (tail = @tail).nil?
+        @tail = [node] of ASTNode
+      else
+        tail.push(node)
+      end
+      self
+    end
+
+    def reject!(& : ASTNode ->) : self
+      if first = @first
+        if second = @second
+          if tail = @tail
+            tail.reject! { |node| yield node }
+          end
+          if yield second
+            @second = tail.try &.shift?
+          end
+        end
+        if yield first
+          @first = @second
+          @second = tail.try &.shift?
+        end
+      end
+      self
+    end
+
+    def concat(nodes : Enumerable(ASTNode)) : self
+      nodes.each { |node| self.push(node) }
+      self
+    end
+  end
+
   class ASTNode
-    property! dependencies : Array(ASTNode)
-    property observers : Array(ASTNode)?
+    getter dependencies : SmallNodeList = SmallNodeList.new
+    @observers : SmallNodeList = SmallNodeList.new
     property enclosing_call : Call?
 
     @dirty = false
@@ -107,8 +177,8 @@ module Crystal
     end
 
     def bind_to(node : ASTNode) : Nil
-      bind(node) do |dependencies|
-        dependencies.push node
+      bind(node) do
+        @dependencies.push node
         node.add_observer self
       end
     end
@@ -116,8 +186,8 @@ module Crystal
     def bind_to(nodes : Indexable) : Nil
       return if nodes.empty?
 
-      bind do |dependencies|
-        dependencies.concat nodes
+      bind do
+        @dependencies.concat nodes
         nodes.each &.add_observer self
       end
     end
@@ -130,9 +200,7 @@ module Crystal
         raise_frozen_type freeze_type, from_type, from
       end
 
-      dependencies = @dependencies ||= [] of ASTNode
-
-      yield dependencies
+      yield
 
       new_type = type_from_dependencies
       new_type = map_type(new_type) if new_type
@@ -150,7 +218,7 @@ module Crystal
     end
 
     def type_from_dependencies : Type?
-      Type.merge dependencies
+      Type.merge @dependencies
     end
 
     def unbind_from(nodes : Nil)
@@ -158,18 +226,17 @@ module Crystal
     end
 
     def unbind_from(node : ASTNode)
-      @dependencies.try &.reject! &.same?(node)
+      @dependencies.reject! &.same?(node)
       node.remove_observer self
     end
 
-    def unbind_from(nodes : Array(ASTNode))
-      @dependencies.try &.reject! { |dep| nodes.any? &.same?(dep) }
+    def unbind_from(nodes : Enumerable(ASTNode))
+      @dependencies.reject! { |dep| nodes.any? &.same?(dep) }
       nodes.each &.remove_observer self
     end
 
     def add_observer(observer)
-      observers = @observers ||= [] of ASTNode
-      observers.push observer
+      @observers.push observer
     end
 
     def remove_observer(observer)
@@ -269,16 +336,10 @@ module Crystal
       visited = Set(ASTNode).new.compare_by_identity
       owner_trace << node if node.type?.try &.includes_type?(owner)
       visited.add node
-      while deps = node.dependencies?
-        dependencies = deps.select { |dep| dep.type? && dep.type.includes_type?(owner) && !visited.includes?(dep) }
-        if dependencies.size > 0
-          node = dependencies.first
-          nil_reason = node.nil_reason if node.is_a?(MetaTypeVar)
-          owner_trace << node if node
-          visited.add node
-        else
-          break
-        end
+      while node = node.dependencies.find { |dep| dep.type? && dep.type.includes_type?(owner) && !visited.includes?(dep) }
+        nil_reason = node.nil_reason if node.is_a?(MetaTypeVar)
+        owner_trace << node if node
+        visited.add node
       end
 
       MethodTraceException.new(owner, owner_trace, nil_reason, program.show_error_trace?)
diff --git a/src/compiler/crystal/semantic/call_error.cr b/src/compiler/crystal/semantic/call_error.cr
index aee5b9e2019b..d19be20afbad 100644
--- a/src/compiler/crystal/semantic/call_error.cr
+++ b/src/compiler/crystal/semantic/call_error.cr
@@ -643,8 +643,7 @@ class Crystal::Call
       if obj.is_a?(InstanceVar)
         scope = self.scope
         ivar = scope.lookup_instance_var(obj.name)
-        deps = ivar.dependencies?
-        if deps && deps.size == 1 && deps.first.same?(program.nil_var)
+        if ivar.dependencies.size == 1 && ivar.dependencies.first.same?(program.nil_var)
           similar_name = scope.lookup_similar_instance_var_name(ivar.name)
           if similar_name
             msg << colorize(" (#{ivar.name} was never assigned a value, did you mean #{similar_name}?)").yellow.bold
diff --git a/src/compiler/crystal/semantic/cleanup_transformer.cr b/src/compiler/crystal/semantic/cleanup_transformer.cr
index 541e0f51d662..054c7871bd8e 100644
--- a/src/compiler/crystal/semantic/cleanup_transformer.cr
+++ b/src/compiler/crystal/semantic/cleanup_transformer.cr
@@ -1090,10 +1090,7 @@ module Crystal
       node = super
 
       unless node.type?
-        if dependencies = node.dependencies?
-          node.unbind_from node.dependencies
-        end
-
+        node.unbind_from node.dependencies
         node.bind_to node.expressions
       end
 
diff --git a/src/compiler/crystal/semantic/filters.cr b/src/compiler/crystal/semantic/filters.cr
index 66d1a728804b..7dd253fc2292 100644
--- a/src/compiler/crystal/semantic/filters.cr
+++ b/src/compiler/crystal/semantic/filters.cr
@@ -1,7 +1,7 @@
 module Crystal
   class TypeFilteredNode < ASTNode
     def initialize(@filter : TypeFilter, @node : ASTNode)
-      @dependencies = [@node] of ASTNode
+      @dependencies.push @node
       node.add_observer self
       update(@node)
     end
diff --git a/src/compiler/crystal/semantic/main_visitor.cr b/src/compiler/crystal/semantic/main_visitor.cr
index 905d5bac8cb1..efd76f76f056 100644
--- a/src/compiler/crystal/semantic/main_visitor.cr
+++ b/src/compiler/crystal/semantic/main_visitor.cr
@@ -373,7 +373,7 @@ module Crystal
           var.bind_to(@program.nil_var)
           var.nil_if_read = false
 
-          meta_var.bind_to(@program.nil_var) unless meta_var.dependencies.try &.any? &.same?(@program.nil_var)
+          meta_var.bind_to(@program.nil_var) unless meta_var.dependencies.any? &.same?(@program.nil_var)
           node.bind_to(@program.nil_var)
         end
 
@@ -1283,7 +1283,7 @@ module Crystal
         # It can happen that this call is inside an ArrayLiteral or HashLiteral,
         # was expanded but isn't bound to the expansion because the call (together
         # with its expansion) was cloned.
-        if (expanded = node.expanded) && (!node.dependencies? || !node.type?)
+        if (expanded = node.expanded) && (node.dependencies.empty? || !node.type?)
           node.bind_to(expanded)
         end
 
diff --git a/src/compiler/crystal/semantic/type_merge.cr b/src/compiler/crystal/semantic/type_merge.cr
index 874949dd516d..67e9f1b61911 100644
--- a/src/compiler/crystal/semantic/type_merge.cr
+++ b/src/compiler/crystal/semantic/type_merge.cr
@@ -17,7 +17,7 @@ module Crystal
       end
     end
 
-    def type_merge(nodes : Array(ASTNode)) : Type?
+    def type_merge(nodes : Enumerable(ASTNode)) : Type?
       case nodes.size
       when 0
         nil
@@ -25,8 +25,10 @@ module Crystal
         nodes.first.type?
       when 2
         # Merging two types is the most common case, so we optimize it
-        first, second = nodes
-        type_merge(first.type?, second.type?)
+        # We use `#each_cons_pair` to avoid any intermediate allocation
+        nodes.each_cons_pair do |first, second|
+          return type_merge(first.type?, second.type?)
+        end
       else
         combined_union_of compact_types(nodes, &.type?)
       end
@@ -161,7 +163,7 @@ module Crystal
   end
 
   class Type
-    def self.merge(nodes : Array(ASTNode)) : Type?
+    def self.merge(nodes : Enumerable(ASTNode)) : Type?
       nodes.find(&.type?).try &.type.program.type_merge(nodes)
     end
 

From f2a6628672557ff95b645f67fee8dd6b3092f667 Mon Sep 17 00:00:00 2001
From: Quinton Miller <nicetas.c@gmail.com>
Date: Wed, 23 Oct 2024 16:38:35 +0800
Subject: [PATCH 2/7] Add CI workflow for cross-compiling Crystal on MSYS2
 (#15110)

Cross-compiles a MinGW-w64-based Crystal compiler from Ubuntu, then links it on MSYS2's UCRT64 environment. Resolves part of #6170.

The artifact includes the compiler, all dependent DLLs, and the source code only. It is not a complete installation since it is missing e.g. the documentation and the licenses, but it is sufficient for bootstrapping further native compiler builds within MSYS2.

The resulting binary is portable within MSYS2 and can be executed from anywhere inside an MSYS2 shell, although compilation requires `mingw-w64-ucrt-x86_64-cc`, probably `mingw-w64-ucrt-x86_64-pkgconf`, plus the respective development libraries listed in #15077. The DLLs bundled under `bin/` are needed to even start Crystal since they are dynamically linked at load time; they are not strictly needed if Crystal is always run only within MSYS2, but that is the job of an actual `PKGBUILD` file.
---
 .github/workflows/mingw-w64.yml | 91 +++++++++++++++++++++++++++++++++
 1 file changed, 91 insertions(+)
 create mode 100644 .github/workflows/mingw-w64.yml

diff --git a/.github/workflows/mingw-w64.yml b/.github/workflows/mingw-w64.yml
new file mode 100644
index 000000000000..2370ae133cdd
--- /dev/null
+++ b/.github/workflows/mingw-w64.yml
@@ -0,0 +1,91 @@
+name: MinGW-w64 CI
+
+on: [push, pull_request]
+
+permissions: {}
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
+
+jobs:
+  x86_64-mingw-w64-cross-compile:
+    runs-on: ubuntu-24.04
+    steps:
+      - name: Download Crystal source
+        uses: actions/checkout@v4
+
+      - name: Install LLVM 18
+        run: |
+          sudo apt remove 'llvm-*' 'libllvm*'
+          wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
+          sudo apt-add-repository -y deb http://apt.llvm.org/noble/ llvm-toolchain-noble-18 main
+          sudo apt install -y llvm-18-dev
+
+      - name: Install Crystal
+        uses: crystal-lang/install-crystal@v1
+        with:
+          crystal: "1.14.0"
+
+      - name: Cross-compile Crystal
+        run: make && make -B target=x86_64-windows-gnu release=1
+
+      - name: Upload crystal.obj
+        uses: actions/upload-artifact@v4
+        with:
+          name: x86_64-mingw-w64-crystal-obj
+          path: .build/crystal.obj
+
+      - name: Upload standard library
+        uses: actions/upload-artifact@v4
+        with:
+          name: x86_64-mingw-w64-crystal-stdlib
+          path: src
+
+  x86_64-mingw-w64-link:
+    runs-on: windows-2022
+    needs: [x86_64-mingw-w64-cross-compile]
+    steps:
+      - name: Setup MSYS2
+        id: msys2
+        uses: msys2/setup-msys2@ddf331adaebd714795f1042345e6ca57bd66cea8 # v2.24.1
+        with:
+          msystem: UCRT64
+          update: true
+          install: >-
+            mingw-w64-ucrt-x86_64-pkgconf
+            mingw-w64-ucrt-x86_64-cc
+            mingw-w64-ucrt-x86_64-gc
+            mingw-w64-ucrt-x86_64-pcre2
+            mingw-w64-ucrt-x86_64-libiconv
+            mingw-w64-ucrt-x86_64-zlib
+            mingw-w64-ucrt-x86_64-llvm
+
+      - name: Download crystal.obj
+        uses: actions/download-artifact@v4
+        with:
+          name: x86_64-mingw-w64-crystal-obj
+
+      - name: Download standard library
+        uses: actions/download-artifact@v4
+        with:
+          name: x86_64-mingw-w64-crystal-stdlib
+          path: share/crystal/src
+
+      - name: Link Crystal executable
+        shell: msys2 {0}
+        run: |
+          mkdir bin
+          cc crystal.obj -o bin/crystal.exe \
+            $(pkg-config bdw-gc libpcre2-8 iconv zlib --libs) \
+            $(llvm-config --libs --system-libs --ldflags) \
+            -lDbgHelp -lole32 -lWS2_32 -Wl,--stack,0x800000
+          ldd bin/crystal.exe | grep -iv /c/windows/system32 | sed 's/.* => //; s/ (.*//' | xargs -t -i cp '{}' bin/
+
+      - name: Upload Crystal
+        uses: actions/upload-artifact@v4
+        with:
+          name: x86_64-mingw-w64-crystal
+          path: |
+            bin/
+            share/

From b8475639fd087b00db5759f0a574ccf5fd67a18d Mon Sep 17 00:00:00 2001
From: Julien Portalier <julien@portalier.com>
Date: Wed, 23 Oct 2024 23:26:26 +0200
Subject: [PATCH 3/7] Fix: LibC bindings and std specs on NetBSD 10 (#15115)

The LibC bindings for NetBSD were a bit wrong and some std specs also didn't work as expected. The segfault handler is also broken on NetBSD (the process crashes with SIGILL after receiving SIGSEGV).

With these fixes + pending specs I can run the std and compiler test suites in a NetBSD 10.0 VM.

**Caveat**: the pkgsrc for LLVM enforces partial RELRO and linking the std specs from crystal fails. You must pass `--single-module` for the executable to be able to start without missing runtime symbols from libxml2. See #11046.
---
 spec/std/io/io_spec.cr                |  2 +-
 spec/std/kernel_spec.cr               | 62 +++++++++++++++------------
 spec/std/socket/tcp_server_spec.cr    |  4 +-
 spec/std/socket/tcp_socket_spec.cr    |  6 +--
 spec/std/socket/udp_socket_spec.cr    |  3 ++
 spec/std/string_spec.cr               |  8 ++--
 src/crystal/system/unix/dir.cr        |  7 ++-
 src/lib_c/x86_64-netbsd/c/dirent.cr   |  1 -
 src/lib_c/x86_64-netbsd/c/netdb.cr    |  1 +
 src/lib_c/x86_64-netbsd/c/sys/time.cr |  2 +-
 10 files changed, 55 insertions(+), 41 deletions(-)

diff --git a/spec/std/io/io_spec.cr b/spec/std/io/io_spec.cr
index 3be5c07e1479..620a1d034d9f 100644
--- a/spec/std/io/io_spec.cr
+++ b/spec/std/io/io_spec.cr
@@ -736,7 +736,7 @@ describe IO do
         it "says invalid byte sequence" do
           io = SimpleIOMemory.new(Slice.new(1, 255_u8))
           io.set_encoding("EUC-JP")
-          expect_raises ArgumentError, {% if flag?(:musl) || flag?(:freebsd) %}"Incomplete multibyte sequence"{% else %}"Invalid multibyte sequence"{% end %} do
+          expect_raises ArgumentError, {% if flag?(:musl) || flag?(:freebsd) || flag?(:netbsd) %}"Incomplete multibyte sequence"{% else %}"Invalid multibyte sequence"{% end %} do
             io.read_char
           end
         end
diff --git a/spec/std/kernel_spec.cr b/spec/std/kernel_spec.cr
index f41529af901a..7f3c39d9e9ec 100644
--- a/spec/std/kernel_spec.cr
+++ b/spec/std/kernel_spec.cr
@@ -254,38 +254,44 @@ describe "hardware exception" do
     error.should_not contain("Stack overflow")
   end
 
-  it "detects stack overflow on the main stack", tags: %w[slow] do
-    # This spec can take some time under FreeBSD where
-    # the default stack size is 0.5G.  Setting a
-    # smaller stack size with `ulimit -s 8192`
-    # will address this.
-    status, _, error = compile_and_run_source <<-'CRYSTAL'
-      def foo
-        y = StaticArray(Int8, 512).new(0)
+  {% if flag?(:netbsd) %}
+    # FIXME: on netbsd the process crashes with SIGILL after receiving SIGSEGV
+    pending "detects stack overflow on the main stack"
+    pending "detects stack overflow on a fiber stack"
+  {% else %}
+    it "detects stack overflow on the main stack", tags: %w[slow] do
+      # This spec can take some time under FreeBSD where
+      # the default stack size is 0.5G.  Setting a
+      # smaller stack size with `ulimit -s 8192`
+      # will address this.
+      status, _, error = compile_and_run_source <<-'CRYSTAL'
+        def foo
+          y = StaticArray(Int8, 512).new(0)
+          foo
+        end
         foo
-      end
-      foo
-    CRYSTAL
+      CRYSTAL
 
-    status.success?.should be_false
-    error.should contain("Stack overflow")
-  end
+      status.success?.should be_false
+      error.should contain("Stack overflow")
+    end
 
-  it "detects stack overflow on a fiber stack", tags: %w[slow] do
-    status, _, error = compile_and_run_source <<-'CRYSTAL'
-      def foo
-        y = StaticArray(Int8, 512).new(0)
-        foo
-      end
+    it "detects stack overflow on a fiber stack", tags: %w[slow] do
+      status, _, error = compile_and_run_source <<-'CRYSTAL'
+        def foo
+          y = StaticArray(Int8, 512).new(0)
+          foo
+        end
 
-      spawn do
-        foo
-      end
+        spawn do
+          foo
+        end
 
-      sleep 60.seconds
-    CRYSTAL
+        sleep 60.seconds
+      CRYSTAL
 
-    status.success?.should be_false
-    error.should contain("Stack overflow")
-  end
+      status.success?.should be_false
+      error.should contain("Stack overflow")
+    end
+  {% end %}
 end
diff --git a/spec/std/socket/tcp_server_spec.cr b/spec/std/socket/tcp_server_spec.cr
index 0c6113a4a7ff..ee3c861956b8 100644
--- a/spec/std/socket/tcp_server_spec.cr
+++ b/spec/std/socket/tcp_server_spec.cr
@@ -96,7 +96,7 @@ describe TCPServer, tags: "network" do
         # FIXME: Resolve special handling for win32. The error code handling should be identical.
         {% if flag?(:win32) %}
           [WinError::WSAHOST_NOT_FOUND, WinError::WSATRY_AGAIN].should contain err.os_error
-        {% elsif flag?(:android) %}
+        {% elsif flag?(:android) || flag?(:netbsd) %}
           err.os_error.should eq(Errno.new(LibC::EAI_NODATA))
         {% else %}
           [Errno.new(LibC::EAI_NONAME), Errno.new(LibC::EAI_AGAIN)].should contain err.os_error
@@ -110,7 +110,7 @@ describe TCPServer, tags: "network" do
         # FIXME: Resolve special handling for win32. The error code handling should be identical.
         {% if flag?(:win32) %}
           [WinError::WSAHOST_NOT_FOUND, WinError::WSATRY_AGAIN].should contain err.os_error
-        {% elsif flag?(:android) %}
+        {% elsif flag?(:android) || flag?(:netbsd) %}
           err.os_error.should eq(Errno.new(LibC::EAI_NODATA))
         {% else %}
           [Errno.new(LibC::EAI_NONAME), Errno.new(LibC::EAI_AGAIN)].should contain err.os_error
diff --git a/spec/std/socket/tcp_socket_spec.cr b/spec/std/socket/tcp_socket_spec.cr
index f3d460f92401..5ec3467362e0 100644
--- a/spec/std/socket/tcp_socket_spec.cr
+++ b/spec/std/socket/tcp_socket_spec.cr
@@ -79,7 +79,7 @@ describe TCPSocket, tags: "network" do
         # FIXME: Resolve special handling for win32. The error code handling should be identical.
         {% if flag?(:win32) %}
           [WinError::WSAHOST_NOT_FOUND, WinError::WSATRY_AGAIN].should contain err.os_error
-        {% elsif flag?(:android) %}
+        {% elsif flag?(:android) || flag?(:netbsd) %}
           err.os_error.should eq(Errno.new(LibC::EAI_NODATA))
         {% else %}
           [Errno.new(LibC::EAI_NONAME), Errno.new(LibC::EAI_AGAIN)].should contain err.os_error
@@ -93,7 +93,7 @@ describe TCPSocket, tags: "network" do
         # FIXME: Resolve special handling for win32. The error code handling should be identical.
         {% if flag?(:win32) %}
           [WinError::WSAHOST_NOT_FOUND, WinError::WSATRY_AGAIN].should contain err.os_error
-        {% elsif flag?(:android) %}
+        {% elsif flag?(:android) || flag?(:netbsd) %}
           err.os_error.should eq(Errno.new(LibC::EAI_NODATA))
         {% else %}
           [Errno.new(LibC::EAI_NONAME), Errno.new(LibC::EAI_AGAIN)].should contain err.os_error
@@ -142,7 +142,7 @@ describe TCPSocket, tags: "network" do
         (client.tcp_nodelay = false).should be_false
         client.tcp_nodelay?.should be_false
 
-        {% unless flag?(:openbsd) %}
+        {% unless flag?(:openbsd) || flag?(:netbsd) %}
           (client.tcp_keepalive_idle = 42).should eq 42
           client.tcp_keepalive_idle.should eq 42
           (client.tcp_keepalive_interval = 42).should eq 42
diff --git a/spec/std/socket/udp_socket_spec.cr b/spec/std/socket/udp_socket_spec.cr
index 6e4b607b80ea..9b624110fad9 100644
--- a/spec/std/socket/udp_socket_spec.cr
+++ b/spec/std/socket/udp_socket_spec.cr
@@ -85,6 +85,9 @@ describe UDPSocket, tags: "network" do
     elsif {{ flag?(:freebsd) }} && family == Socket::Family::INET6
       # FIXME: fails with "Error sending datagram to [ipv6]:port: Network is unreachable"
       pending "joins and transmits to multicast groups"
+    elsif {{ flag?(:netbsd) }} && family == Socket::Family::INET6
+      # FIXME: fails with "setsockopt: EADDRNOTAVAIL"
+      pending "joins and transmits to multicast groups"
     else
       it "joins and transmits to multicast groups" do
         udp = UDPSocket.new(family)
diff --git a/spec/std/string_spec.cr b/spec/std/string_spec.cr
index 2ffe5bf3d1fa..6d7487ded0e2 100644
--- a/spec/std/string_spec.cr
+++ b/spec/std/string_spec.cr
@@ -2830,7 +2830,7 @@ describe "String" do
         bytes.to_a.should eq([72, 0, 101, 0, 108, 0, 108, 0, 111, 0])
       end
 
-      {% unless flag?(:musl) || flag?(:solaris) || flag?(:freebsd) || flag?(:dragonfly) %}
+      {% unless flag?(:musl) || flag?(:solaris) || flag?(:freebsd) || flag?(:dragonfly) || flag?(:netbsd) %}
         it "flushes the shift state (#11992)" do
           "\u{00CA}".encode("BIG5-HKSCS").should eq(Bytes[0x88, 0x66])
           "\u{00CA}\u{0304}".encode("BIG5-HKSCS").should eq(Bytes[0x88, 0x62])
@@ -2839,7 +2839,7 @@ describe "String" do
 
       # FreeBSD iconv encoder expects ISO/IEC 10646 compatibility code points,
       # see https://www.ccli.gov.hk/doc/e_hkscs_2008.pdf for details.
-      {% if flag?(:freebsd) || flag?(:dragonfly) %}
+      {% if flag?(:freebsd) || flag?(:dragonfly) || flag?(:netbsd) %}
         it "flushes the shift state (#11992)" do
           "\u{F329}".encode("BIG5-HKSCS").should eq(Bytes[0x88, 0x66])
           "\u{F325}".encode("BIG5-HKSCS").should eq(Bytes[0x88, 0x62])
@@ -2883,7 +2883,7 @@ describe "String" do
         String.new(bytes, "UTF-16LE").should eq("Hello")
       end
 
-      {% unless flag?(:solaris) || flag?(:freebsd) || flag?(:dragonfly) %}
+      {% unless flag?(:solaris) || flag?(:freebsd) || flag?(:dragonfly) || flag?(:netbsd) %}
         it "decodes with shift state" do
           String.new(Bytes[0x88, 0x66], "BIG5-HKSCS").should eq("\u{00CA}")
           String.new(Bytes[0x88, 0x62], "BIG5-HKSCS").should eq("\u{00CA}\u{0304}")
@@ -2892,7 +2892,7 @@ describe "String" do
 
       # FreeBSD iconv decoder returns ISO/IEC 10646-1:2000 code points,
       # see https://www.ccli.gov.hk/doc/e_hkscs_2008.pdf for details.
-      {% if flag?(:freebsd) || flag?(:dragonfly) %}
+      {% if flag?(:freebsd) || flag?(:dragonfly) || flag?(:netbsd) %}
         it "decodes with shift state" do
           String.new(Bytes[0x88, 0x66], "BIG5-HKSCS").should eq("\u{00CA}")
           String.new(Bytes[0x88, 0x62], "BIG5-HKSCS").should eq("\u{F325}")
diff --git a/src/crystal/system/unix/dir.cr b/src/crystal/system/unix/dir.cr
index 5e66b33b65e7..72d1183dcc72 100644
--- a/src/crystal/system/unix/dir.cr
+++ b/src/crystal/system/unix/dir.cr
@@ -42,7 +42,12 @@ module Crystal::System::Dir
   end
 
   def self.info(dir, path) : ::File::Info
-    Crystal::System::FileDescriptor.system_info LibC.dirfd(dir)
+    fd = {% if flag?(:netbsd) %}
+           dir.value.dd_fd
+         {% else %}
+           LibC.dirfd(dir)
+         {% end %}
+    Crystal::System::FileDescriptor.system_info(fd)
   end
 
   def self.close(dir, path) : Nil
diff --git a/src/lib_c/x86_64-netbsd/c/dirent.cr b/src/lib_c/x86_64-netbsd/c/dirent.cr
index 71dabe7b08ce..e3b8492083f7 100644
--- a/src/lib_c/x86_64-netbsd/c/dirent.cr
+++ b/src/lib_c/x86_64-netbsd/c/dirent.cr
@@ -29,5 +29,4 @@ lib LibC
   fun opendir = __opendir30(x0 : Char*) : DIR*
   fun readdir = __readdir30(x0 : DIR*) : Dirent*
   fun rewinddir(x0 : DIR*) : Void
-  fun dirfd(dirp : DIR*) : Int
 end
diff --git a/src/lib_c/x86_64-netbsd/c/netdb.cr b/src/lib_c/x86_64-netbsd/c/netdb.cr
index 4443325cd487..c098ab2f5fc6 100644
--- a/src/lib_c/x86_64-netbsd/c/netdb.cr
+++ b/src/lib_c/x86_64-netbsd/c/netdb.cr
@@ -13,6 +13,7 @@ lib LibC
   EAI_FAIL       =     4
   EAI_FAMILY     =     5
   EAI_MEMORY     =     6
+  EAI_NODATA     =     7
   EAI_NONAME     =     8
   EAI_SERVICE    =     9
   EAI_SOCKTYPE   =    10
diff --git a/src/lib_c/x86_64-netbsd/c/sys/time.cr b/src/lib_c/x86_64-netbsd/c/sys/time.cr
index f276784708c0..3bb54d42c5cd 100644
--- a/src/lib_c/x86_64-netbsd/c/sys/time.cr
+++ b/src/lib_c/x86_64-netbsd/c/sys/time.cr
@@ -13,5 +13,5 @@ lib LibC
 
   fun gettimeofday = __gettimeofday50(x0 : Timeval*, x1 : Timezone*) : Int
   fun utimes = __utimes50(path : Char*, times : Timeval[2]) : Int
-  fun futimens = __futimens50(fd : Int, times : Timespec[2]) : Int
+  fun futimens(fd : Int, times : Timespec[2]) : Int
 end

From 2e8715d67a568ad4d4ea3d703fd8a0bbf2fe5434 Mon Sep 17 00:00:00 2001
From: Quinton Miller <nicetas.c@gmail.com>
Date: Thu, 24 Oct 2024 05:27:02 +0800
Subject: [PATCH 4/7] Disable specs that break on MinGW-w64 (#15116)

These specs do not have straightforward workarounds when run under MSYS2.
---
 spec/compiler/codegen/pointer_spec.cr      | 53 ++++++++++++----------
 spec/compiler/codegen/thread_local_spec.cr |  2 +-
 spec/std/kernel_spec.cr                    |  8 ++++
 3 files changed, 38 insertions(+), 25 deletions(-)

diff --git a/spec/compiler/codegen/pointer_spec.cr b/spec/compiler/codegen/pointer_spec.cr
index 1230d80cb5f6..da132cdee406 100644
--- a/spec/compiler/codegen/pointer_spec.cr
+++ b/spec/compiler/codegen/pointer_spec.cr
@@ -492,28 +492,33 @@ describe "Code gen: pointer" do
       )).to_b.should be_true
   end
 
-  it "takes pointerof lib external var" do
-    test_c(
-      %(
-        int external_var = 0;
-      ),
-      %(
-        lib LibFoo
-          $external_var : Int32
-        end
-
-        LibFoo.external_var = 1
-
-        ptr = pointerof(LibFoo.external_var)
-        x = ptr.value
-
-        ptr.value = 10
-        y = ptr.value
-
-        ptr.value = 100
-        z = LibFoo.external_var
-
-        x + y + z
-      ), &.to_i.should eq(111))
-  end
+  # FIXME: `$external_var` implies __declspec(dllimport), but we only have an
+  # object file, so MinGW-w64 fails linking (actually MSVC also emits an
+  # LNK4217 linker warning)
+  {% unless flag?(:win32) && flag?(:gnu) %}
+    it "takes pointerof lib external var" do
+      test_c(
+        %(
+          int external_var = 0;
+        ),
+        %(
+          lib LibFoo
+            $external_var : Int32
+          end
+
+          LibFoo.external_var = 1
+
+          ptr = pointerof(LibFoo.external_var)
+          x = ptr.value
+
+          ptr.value = 10
+          y = ptr.value
+
+          ptr.value = 100
+          z = LibFoo.external_var
+
+          x + y + z
+        ), &.to_i.should eq(111))
+    end
+  {% end %}
 end
diff --git a/spec/compiler/codegen/thread_local_spec.cr b/spec/compiler/codegen/thread_local_spec.cr
index 694cb430b8c1..386043f2c5fd 100644
--- a/spec/compiler/codegen/thread_local_spec.cr
+++ b/spec/compiler/codegen/thread_local_spec.cr
@@ -1,4 +1,4 @@
-{% skip_file if flag?(:openbsd) %}
+{% skip_file if flag?(:openbsd) || (flag?(:win32) && flag?(:gnu)) %}
 
 require "../../spec_helper"
 
diff --git a/spec/std/kernel_spec.cr b/spec/std/kernel_spec.cr
index 7f3c39d9e9ec..f8e4ff1e8ae2 100644
--- a/spec/std/kernel_spec.cr
+++ b/spec/std/kernel_spec.cr
@@ -8,6 +8,14 @@ describe "PROGRAM_NAME" do
         pending! "Example is broken in Nix shell (#12332)"
       end
 
+      # MSYS2: gcc/ld doesn't support unicode paths
+      # https://github.com/msys2/MINGW-packages/issues/17812
+      {% if flag?(:windows) %}
+        if ENV["MSYSTEM"]?
+          pending! "Example is broken in MSYS2 shell"
+        end
+      {% end %}
+
       File.write(source_file, "File.basename(PROGRAM_NAME).inspect(STDOUT)")
 
       compile_file(source_file, bin_name: "×‽😂") do |executable_file|

From 94386b640c4d70305bf12f7da9e2184694277587 Mon Sep 17 00:00:00 2001
From: Quinton Miller <nicetas.c@gmail.com>
Date: Thu, 24 Oct 2024 05:27:25 +0800
Subject: [PATCH 5/7] Treat `WinError::ERROR_DIRECTORY` as an error for
 non-existent files (#15114)

---
 src/crystal/system/win32/file.cr | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/crystal/system/win32/file.cr b/src/crystal/system/win32/file.cr
index 7b7b443ce310..b6f9cf2b7ccd 100644
--- a/src/crystal/system/win32/file.cr
+++ b/src/crystal/system/win32/file.cr
@@ -116,6 +116,7 @@ module Crystal::System::File
     WinError::ERROR_FILE_NOT_FOUND,
     WinError::ERROR_PATH_NOT_FOUND,
     WinError::ERROR_INVALID_NAME,
+    WinError::ERROR_DIRECTORY,
   }
 
   def self.check_not_found_error(message, path)

From 454744a35e34fdd995c22200a32f67a2b4320f1c Mon Sep 17 00:00:00 2001
From: Quinton Miller <nicetas.c@gmail.com>
Date: Thu, 24 Oct 2024 18:30:42 +0800
Subject: [PATCH 6/7] Support call stacks for MinGW-w64 builds (#15117)

Introduces new methods for extracting COFF debug information from programs in the PE format, integrating them into Crystal's existing DWARF parsing functionality. Resolves part of #6170.

It is questionable whether reusing `src/exception/call_stack/elf.cr` for MinGW-w64 is appropriate, since nothing here is in the ELF format, but this PR tries to avoid moving existing code around, save for the old `Exception::CallStack.setup_crash_handler` as it remains the only common portion between MSVC and MinGW-w64.
---
 spec/std/exception/call_stack_spec.cr         |   6 +-
 src/crystal/pe.cr                             | 110 +++++++++++++++++
 src/crystal/system/win32/signal.cr            |  44 +++++++
 src/exception/call_stack.cr                   |   5 +-
 src/exception/call_stack/dwarf.cr             |   4 +
 src/exception/call_stack/elf.cr               |  86 +++++++------
 src/exception/call_stack/libunwind.cr         | 113 ++++++++++++++++--
 src/exception/call_stack/stackwalk.cr         |  61 +---------
 .../x86_64-windows-msvc/c/libloaderapi.cr     |   3 +
 src/lib_c/x86_64-windows-msvc/c/winnt.cr      |  54 +++++++++
 src/raise.cr                                  |   2 +-
 11 files changed, 379 insertions(+), 109 deletions(-)
 create mode 100644 src/crystal/pe.cr

diff --git a/spec/std/exception/call_stack_spec.cr b/spec/std/exception/call_stack_spec.cr
index c01fb0ff6b8a..7c6f5d746bdc 100644
--- a/spec/std/exception/call_stack_spec.cr
+++ b/spec/std/exception/call_stack_spec.cr
@@ -12,9 +12,9 @@ describe "Backtrace" do
 
     _, output, _ = compile_and_run_file(source_file)
 
-    # resolved file:line:column (no column for windows PDB because of poor
-    # support in general)
-    {% if flag?(:win32) %}
+    # resolved file:line:column (no column for MSVC PDB because of poor support
+    # by external tooling in general)
+    {% if flag?(:msvc) %}
       output.should match(/^#{Regex.escape(source_file)}:3 in 'callee1'/m)
       output.should match(/^#{Regex.escape(source_file)}:13 in 'callee3'/m)
     {% else %}
diff --git a/src/crystal/pe.cr b/src/crystal/pe.cr
new file mode 100644
index 000000000000..d1b19401ad19
--- /dev/null
+++ b/src/crystal/pe.cr
@@ -0,0 +1,110 @@
+module Crystal
+  # :nodoc:
+  #
+  # Portable Executable reader.
+  #
+  # Documentation:
+  # - <https://learn.microsoft.com/en-us/windows/win32/debug/pe-format>
+  struct PE
+    class Error < Exception
+    end
+
+    record SectionHeader, name : String, virtual_offset : UInt32, offset : UInt32, size : UInt32
+
+    record COFFSymbol, offset : UInt32, name : String
+
+    # addresses in COFF debug info are relative to this image base; used by
+    # `Exception::CallStack.read_dwarf_sections` to calculate the real relocated
+    # addresses
+    getter original_image_base : UInt64
+
+    @section_headers : Slice(SectionHeader)
+    @string_table_base : UInt32
+
+    # mapping from zero-based section index to list of symbols sorted by
+    # offsets within that section
+    getter coff_symbols = Hash(Int32, Array(COFFSymbol)).new
+
+    def self.open(path : String | ::Path, &)
+      File.open(path, "r") do |file|
+        yield new(file)
+      end
+    end
+
+    def initialize(@io : IO::FileDescriptor)
+      dos_header = uninitialized LibC::IMAGE_DOS_HEADER
+      io.read_fully(pointerof(dos_header).to_slice(1).to_unsafe_bytes)
+      raise Error.new("Invalid DOS header") unless dos_header.e_magic == 0x5A4D # MZ
+
+      io.seek(dos_header.e_lfanew)
+      nt_header = uninitialized LibC::IMAGE_NT_HEADERS
+      io.read_fully(pointerof(nt_header).to_slice(1).to_unsafe_bytes)
+      raise Error.new("Invalid PE header") unless nt_header.signature == 0x00004550 # PE\0\0
+
+      @original_image_base = nt_header.optionalHeader.imageBase
+      @string_table_base = nt_header.fileHeader.pointerToSymbolTable + nt_header.fileHeader.numberOfSymbols * sizeof(LibC::IMAGE_SYMBOL)
+
+      section_count = nt_header.fileHeader.numberOfSections
+      nt_section_headers = Pointer(LibC::IMAGE_SECTION_HEADER).malloc(section_count).to_slice(section_count)
+      io.read_fully(nt_section_headers.to_unsafe_bytes)
+
+      @section_headers = nt_section_headers.map do |nt_header|
+        if nt_header.name[0] === '/'
+          # section name is longer than 8 bytes; look up the COFF string table
+          name_buf = nt_header.name.to_slice + 1
+          string_offset = String.new(name_buf.to_unsafe, name_buf.index(0) || name_buf.size).to_i
+          io.seek(@string_table_base + string_offset)
+          name = io.gets('\0', chomp: true).not_nil!
+        else
+          name = String.new(nt_header.name.to_unsafe, nt_header.name.index(0) || nt_header.name.size)
+        end
+
+        SectionHeader.new(name: name, virtual_offset: nt_header.virtualAddress, offset: nt_header.pointerToRawData, size: nt_header.virtualSize)
+      end
+
+      io.seek(nt_header.fileHeader.pointerToSymbolTable)
+      image_symbol_count = nt_header.fileHeader.numberOfSymbols
+      image_symbols = Pointer(LibC::IMAGE_SYMBOL).malloc(image_symbol_count).to_slice(image_symbol_count)
+      io.read_fully(image_symbols.to_unsafe_bytes)
+
+      aux_count = 0
+      image_symbols.each_with_index do |sym, i|
+        if aux_count == 0
+          aux_count = sym.numberOfAuxSymbols.to_i
+        else
+          aux_count &-= 1
+        end
+
+        next unless aux_count == 0
+        next unless sym.type.bits_set?(0x20) # COFF function
+        next unless sym.sectionNumber > 0    # one-based section index
+        next unless sym.storageClass.in?(LibC::IMAGE_SYM_CLASS_EXTERNAL, LibC::IMAGE_SYM_CLASS_STATIC)
+
+        if sym.n.name.short == 0
+          io.seek(@string_table_base + sym.n.name.long)
+          name = io.gets('\0', chomp: true).not_nil!
+        else
+          name = String.new(sym.n.shortName.to_slice).rstrip('\0')
+        end
+
+        # `@coff_symbols` uses zero-based indices
+        section_coff_symbols = @coff_symbols.put_if_absent(sym.sectionNumber.to_i &- 1) { [] of COFFSymbol }
+        section_coff_symbols << COFFSymbol.new(sym.value, name)
+      end
+
+      # add one sentinel symbol to ensure binary search on the offsets works
+      @coff_symbols.each_with_index do |(_, symbols), i|
+        symbols.sort_by!(&.offset)
+        symbols << COFFSymbol.new(@section_headers[i].size, "??")
+      end
+    end
+
+    def read_section?(name : String, &)
+      if sh = @section_headers.find(&.name.== name)
+        @io.seek(sh.offset) do
+          yield sh, @io
+        end
+      end
+    end
+  end
+end
diff --git a/src/crystal/system/win32/signal.cr b/src/crystal/system/win32/signal.cr
index d805ea4fd1ab..4cebe7cf9c6a 100644
--- a/src/crystal/system/win32/signal.cr
+++ b/src/crystal/system/win32/signal.cr
@@ -1,4 +1,5 @@
 require "c/signal"
+require "c/malloc"
 
 module Crystal::System::Signal
   def self.trap(signal, handler) : Nil
@@ -16,4 +17,47 @@ module Crystal::System::Signal
   def self.ignore(signal) : Nil
     raise NotImplementedError.new("Crystal::System::Signal.ignore")
   end
+
+  def self.setup_seh_handler
+    LibC.AddVectoredExceptionHandler(1, ->(exception_info) do
+      case exception_info.value.exceptionRecord.value.exceptionCode
+      when LibC::EXCEPTION_ACCESS_VIOLATION
+        addr = exception_info.value.exceptionRecord.value.exceptionInformation[1]
+        Crystal::System.print_error "Invalid memory access (C0000005) at address %p\n", Pointer(Void).new(addr)
+        {% if flag?(:gnu) %}
+          Exception::CallStack.print_backtrace
+        {% else %}
+          Exception::CallStack.print_backtrace(exception_info)
+        {% end %}
+        LibC._exit(1)
+      when LibC::EXCEPTION_STACK_OVERFLOW
+        LibC._resetstkoflw
+        Crystal::System.print_error "Stack overflow (e.g., infinite or very deep recursion)\n"
+        {% if flag?(:gnu) %}
+          Exception::CallStack.print_backtrace
+        {% else %}
+          Exception::CallStack.print_backtrace(exception_info)
+        {% end %}
+        LibC._exit(1)
+      else
+        LibC::EXCEPTION_CONTINUE_SEARCH
+      end
+    end)
+
+    # ensure that even in the case of stack overflow there is enough reserved
+    # stack space for recovery (for other threads this is done in
+    # `Crystal::System::Thread.thread_proc`)
+    stack_size = Crystal::System::Fiber::RESERVED_STACK_SIZE
+    LibC.SetThreadStackGuarantee(pointerof(stack_size))
+
+    # this catches invalid argument checks inside the C runtime library
+    LibC._set_invalid_parameter_handler(->(expression, _function, _file, _line, _pReserved) do
+      message = expression ? String.from_utf16(expression)[0] : "(no message)"
+      Crystal::System.print_error "CRT invalid parameter handler invoked: %s\n", message
+      caller.each do |frame|
+        Crystal::System.print_error "  from %s\n", frame
+      end
+      LibC._exit(1)
+    end)
+  end
 end
diff --git a/src/exception/call_stack.cr b/src/exception/call_stack.cr
index 44a281570c1c..506317d2580e 100644
--- a/src/exception/call_stack.cr
+++ b/src/exception/call_stack.cr
@@ -1,10 +1,7 @@
 {% if flag?(:interpreted) %}
   require "./call_stack/interpreter"
-{% elsif flag?(:win32) %}
+{% elsif flag?(:win32) && !flag?(:gnu) %}
   require "./call_stack/stackwalk"
-  {% if flag?(:gnu) %}
-    require "./lib_unwind"
-  {% end %}
 {% elsif flag?(:wasm32) %}
   require "./call_stack/null"
 {% else %}
diff --git a/src/exception/call_stack/dwarf.cr b/src/exception/call_stack/dwarf.cr
index 96d99f03205a..253a72a38ebc 100644
--- a/src/exception/call_stack/dwarf.cr
+++ b/src/exception/call_stack/dwarf.cr
@@ -10,6 +10,10 @@ struct Exception::CallStack
   @@dwarf_line_numbers : Crystal::DWARF::LineNumbers?
   @@dwarf_function_names : Array(Tuple(LibC::SizeT, LibC::SizeT, String))?
 
+  {% if flag?(:win32) %}
+    @@coff_symbols : Hash(Int32, Array(Crystal::PE::COFFSymbol))?
+  {% end %}
+
   # :nodoc:
   def self.load_debug_info : Nil
     return if ENV["CRYSTAL_LOAD_DEBUG_INFO"]? == "0"
diff --git a/src/exception/call_stack/elf.cr b/src/exception/call_stack/elf.cr
index efa54f41329c..51d565528577 100644
--- a/src/exception/call_stack/elf.cr
+++ b/src/exception/call_stack/elf.cr
@@ -1,65 +1,83 @@
-require "crystal/elf"
-{% unless flag?(:wasm32) %}
-  require "c/link"
+{% if flag?(:win32) %}
+  require "crystal/pe"
+{% else %}
+  require "crystal/elf"
+  {% unless flag?(:wasm32) %}
+    require "c/link"
+  {% end %}
 {% end %}
 
 struct Exception::CallStack
-  private struct DlPhdrData
-    getter program : String
-    property base_address : LibC::Elf_Addr = 0
+  {% unless flag?(:win32) %}
+    private struct DlPhdrData
+      getter program : String
+      property base_address : LibC::Elf_Addr = 0
 
-    def initialize(@program : String)
+      def initialize(@program : String)
+      end
     end
-  end
+  {% end %}
 
   protected def self.load_debug_info_impl : Nil
     program = Process.executable_path
     return unless program && File::Info.readable? program
-    data = DlPhdrData.new(program)
-
-    phdr_callback = LibC::DlPhdrCallback.new do |info, size, data|
-      # `dl_iterate_phdr` does not always visit the current program first; on
-      # Android the first object is `/system/bin/linker64`, the second is the
-      # full program path (not the empty string), so we check both here
-      name_c_str = info.value.name
-      if name_c_str && (name_c_str.value == 0 || LibC.strcmp(name_c_str, data.as(DlPhdrData*).value.program) == 0)
-        # The first entry is the header for the current program.
-        # Note that we avoid allocating here and just store the base address
-        # to be passed to self.read_dwarf_sections when dl_iterate_phdr returns.
-        # Calling self.read_dwarf_sections from this callback may lead to reallocations
-        # and deadlocks due to the internal lock held by dl_iterate_phdr (#10084).
-        data.as(DlPhdrData*).value.base_address = info.value.addr
-        1
-      else
-        0
+
+    {% if flag?(:win32) %}
+      if LibC.GetModuleHandleExW(LibC::GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, nil, out hmodule) != 0
+        self.read_dwarf_sections(program, hmodule.address)
       end
-    end
+    {% else %}
+      data = DlPhdrData.new(program)
 
-    LibC.dl_iterate_phdr(phdr_callback, pointerof(data))
-    self.read_dwarf_sections(data.program, data.base_address)
+      phdr_callback = LibC::DlPhdrCallback.new do |info, size, data|
+        # `dl_iterate_phdr` does not always visit the current program first; on
+        # Android the first object is `/system/bin/linker64`, the second is the
+        # full program path (not the empty string), so we check both here
+        name_c_str = info.value.name
+        if name_c_str && (name_c_str.value == 0 || LibC.strcmp(name_c_str, data.as(DlPhdrData*).value.program) == 0)
+          # The first entry is the header for the current program.
+          # Note that we avoid allocating here and just store the base address
+          # to be passed to self.read_dwarf_sections when dl_iterate_phdr returns.
+          # Calling self.read_dwarf_sections from this callback may lead to reallocations
+          # and deadlocks due to the internal lock held by dl_iterate_phdr (#10084).
+          data.as(DlPhdrData*).value.base_address = info.value.addr
+          1
+        else
+          0
+        end
+      end
+
+      LibC.dl_iterate_phdr(phdr_callback, pointerof(data))
+      self.read_dwarf_sections(data.program, data.base_address)
+    {% end %}
   end
 
   protected def self.read_dwarf_sections(program, base_address = 0)
-    Crystal::ELF.open(program) do |elf|
-      line_strings = elf.read_section?(".debug_line_str") do |sh, io|
+    {{ flag?(:win32) ? Crystal::PE : Crystal::ELF }}.open(program) do |image|
+      {% if flag?(:win32) %}
+        base_address -= image.original_image_base
+        @@coff_symbols = image.coff_symbols
+      {% end %}
+
+      line_strings = image.read_section?(".debug_line_str") do |sh, io|
         Crystal::DWARF::Strings.new(io, sh.offset, sh.size)
       end
 
-      strings = elf.read_section?(".debug_str") do |sh, io|
+      strings = image.read_section?(".debug_str") do |sh, io|
         Crystal::DWARF::Strings.new(io, sh.offset, sh.size)
       end
 
-      elf.read_section?(".debug_line") do |sh, io|
+      image.read_section?(".debug_line") do |sh, io|
         @@dwarf_line_numbers = Crystal::DWARF::LineNumbers.new(io, sh.size, base_address, strings, line_strings)
       end
 
-      elf.read_section?(".debug_info") do |sh, io|
+      image.read_section?(".debug_info") do |sh, io|
         names = [] of {LibC::SizeT, LibC::SizeT, String}
 
         while (offset = io.pos - sh.offset) < sh.size
           info = Crystal::DWARF::Info.new(io, offset)
 
-          elf.read_section?(".debug_abbrev") do |sh, io|
+          image.read_section?(".debug_abbrev") do |sh, io|
             info.read_abbreviations(io)
           end
 
diff --git a/src/exception/call_stack/libunwind.cr b/src/exception/call_stack/libunwind.cr
index 1542d52cc736..c0f75867aeba 100644
--- a/src/exception/call_stack/libunwind.cr
+++ b/src/exception/call_stack/libunwind.cr
@@ -1,9 +1,11 @@
-require "c/dlfcn"
+{% unless flag?(:win32) %}
+  require "c/dlfcn"
+{% end %}
 require "c/stdio"
 require "c/string"
 require "../lib_unwind"
 
-{% if flag?(:darwin) || flag?(:bsd) || flag?(:linux) || flag?(:solaris) %}
+{% if flag?(:darwin) || flag?(:bsd) || flag?(:linux) || flag?(:solaris) || flag?(:win32) %}
   require "./dwarf"
 {% else %}
   require "./null"
@@ -33,7 +35,11 @@ struct Exception::CallStack
   {% end %}
 
   def self.setup_crash_handler
-    Crystal::System::Signal.setup_segfault_handler
+    {% if flag?(:win32) %}
+      Crystal::System::Signal.setup_seh_handler
+    {% else %}
+      Crystal::System::Signal.setup_segfault_handler
+    {% end %}
   end
 
   {% if flag?(:interpreted) %} @[Primitive(:interpreter_call_stack_unwind)] {% end %}
@@ -167,9 +173,102 @@ struct Exception::CallStack
     end
   end
 
-  private def self.dladdr(ip, &)
-    if LibC.dladdr(ip, out info) != 0
-      yield info.dli_fname, info.dli_sname, info.dli_saddr
+  {% if flag?(:win32) %}
+    def self.dladdr(ip, &)
+      if LibC.GetModuleHandleExW(LibC::GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT | LibC::GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, ip.as(LibC::LPWSTR), out hmodule) != 0
+        symbol, address = internal_symbol(hmodule, ip) || external_symbol(hmodule, ip) || return
+
+        utf16_file = uninitialized LibC::WCHAR[LibC::MAX_PATH]
+        len = LibC.GetModuleFileNameW(hmodule, utf16_file, utf16_file.size)
+        if 0 < len < utf16_file.size
+          utf8_file = uninitialized UInt8[sizeof(UInt8[LibC::MAX_PATH][3])]
+          file = utf8_file.to_unsafe
+          appender = file.appender
+          String.each_utf16_char(utf16_file.to_slice[0, len + 1]) do |ch|
+            ch.each_byte { |b| appender << b }
+          end
+        else
+          file = Pointer(UInt8).null
+        end
+
+        yield file, symbol, address
+      end
     end
-  end
+
+    private def self.internal_symbol(hmodule, ip)
+      if coff_symbols = @@coff_symbols
+        if LibC.GetModuleHandleExW(LibC::GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, nil, out this_hmodule) != 0 && this_hmodule == hmodule
+          section_base, section_index = lookup_section(hmodule, ip) || return
+          offset = ip - section_base
+          section_coff_symbols = coff_symbols[section_index]? || return
+          next_sym = section_coff_symbols.bsearch_index { |sym| offset < sym.offset } || return
+          sym = section_coff_symbols[next_sym - 1]? || return
+
+          {sym.name.to_unsafe, section_base + sym.offset}
+        end
+      end
+    end
+
+    private def self.external_symbol(hmodule, ip)
+      if dir = data_directory(hmodule, LibC::IMAGE_DIRECTORY_ENTRY_EXPORT)
+        exports = dir.to_unsafe.as(LibC::IMAGE_EXPORT_DIRECTORY*).value
+
+        found_address = Pointer(Void).null
+        found_index = -1
+
+        func_address_offsets = (hmodule + exports.addressOfFunctions).as(LibC::DWORD*).to_slice(exports.numberOfFunctions)
+        func_address_offsets.each_with_index do |offset, i|
+          address = hmodule + offset
+          if found_address < address <= ip
+            found_address, found_index = address, i
+          end
+        end
+
+        return unless found_address
+
+        func_name_ordinals = (hmodule + exports.addressOfNameOrdinals).as(LibC::WORD*).to_slice(exports.numberOfNames)
+        if ordinal_index = func_name_ordinals.index(&.== found_index)
+          symbol = (hmodule + (hmodule + exports.addressOfNames).as(LibC::DWORD*)[ordinal_index]).as(UInt8*)
+          {symbol, found_address}
+        end
+      end
+    end
+
+    private def self.lookup_section(hmodule, ip)
+      dos_header = hmodule.as(LibC::IMAGE_DOS_HEADER*)
+      return unless dos_header.value.e_magic == 0x5A4D # MZ
+
+      nt_header = (hmodule + dos_header.value.e_lfanew).as(LibC::IMAGE_NT_HEADERS*)
+      return unless nt_header.value.signature == 0x00004550 # PE\0\0
+
+      section_headers = (nt_header + 1).as(LibC::IMAGE_SECTION_HEADER*).to_slice(nt_header.value.fileHeader.numberOfSections)
+      section_headers.each_with_index do |header, i|
+        base = hmodule + header.virtualAddress
+        if base <= ip < base + header.virtualSize
+          return base, i
+        end
+      end
+    end
+
+    private def self.data_directory(hmodule, index)
+      dos_header = hmodule.as(LibC::IMAGE_DOS_HEADER*)
+      return unless dos_header.value.e_magic == 0x5A4D # MZ
+
+      nt_header = (hmodule + dos_header.value.e_lfanew).as(LibC::IMAGE_NT_HEADERS*)
+      return unless nt_header.value.signature == 0x00004550 # PE\0\0
+      return unless nt_header.value.optionalHeader.magic == {{ flag?(:bits64) ? 0x20b : 0x10b }}
+      return unless index.in?(0...{16, nt_header.value.optionalHeader.numberOfRvaAndSizes}.min)
+
+      directory = nt_header.value.optionalHeader.dataDirectory.to_unsafe[index]
+      if directory.virtualAddress != 0
+        Bytes.new(hmodule.as(UInt8*) + directory.virtualAddress, directory.size, read_only: true)
+      end
+    end
+  {% else %}
+    private def self.dladdr(ip, &)
+      if LibC.dladdr(ip, out info) != 0
+        yield info.dli_fname, info.dli_sname, info.dli_saddr
+      end
+    end
+  {% end %}
 end
diff --git a/src/exception/call_stack/stackwalk.cr b/src/exception/call_stack/stackwalk.cr
index 6ac59fa6db48..d7e3da8e35f1 100644
--- a/src/exception/call_stack/stackwalk.cr
+++ b/src/exception/call_stack/stackwalk.cr
@@ -1,5 +1,4 @@
 require "c/dbghelp"
-require "c/malloc"
 
 # :nodoc:
 struct Exception::CallStack
@@ -33,38 +32,7 @@ struct Exception::CallStack
   end
 
   def self.setup_crash_handler
-    LibC.AddVectoredExceptionHandler(1, ->(exception_info) do
-      case exception_info.value.exceptionRecord.value.exceptionCode
-      when LibC::EXCEPTION_ACCESS_VIOLATION
-        addr = exception_info.value.exceptionRecord.value.exceptionInformation[1]
-        Crystal::System.print_error "Invalid memory access (C0000005) at address %p\n", Pointer(Void).new(addr)
-        print_backtrace(exception_info)
-        LibC._exit(1)
-      when LibC::EXCEPTION_STACK_OVERFLOW
-        LibC._resetstkoflw
-        Crystal::System.print_error "Stack overflow (e.g., infinite or very deep recursion)\n"
-        print_backtrace(exception_info)
-        LibC._exit(1)
-      else
-        LibC::EXCEPTION_CONTINUE_SEARCH
-      end
-    end)
-
-    # ensure that even in the case of stack overflow there is enough reserved
-    # stack space for recovery (for other threads this is done in
-    # `Crystal::System::Thread.thread_proc`)
-    stack_size = Crystal::System::Fiber::RESERVED_STACK_SIZE
-    LibC.SetThreadStackGuarantee(pointerof(stack_size))
-
-    # this catches invalid argument checks inside the C runtime library
-    LibC._set_invalid_parameter_handler(->(expression, _function, _file, _line, _pReserved) do
-      message = expression ? String.from_utf16(expression)[0] : "(no message)"
-      Crystal::System.print_error "CRT invalid parameter handler invoked: %s\n", message
-      caller.each do |frame|
-        Crystal::System.print_error "  from %s\n", frame
-      end
-      LibC._exit(1)
-    end)
+    Crystal::System::Signal.setup_seh_handler
   end
 
   {% if flag?(:interpreted) %} @[Primitive(:interpreter_call_stack_unwind)] {% end %}
@@ -168,33 +136,6 @@ 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)
diff --git a/src/lib_c/x86_64-windows-msvc/c/libloaderapi.cr b/src/lib_c/x86_64-windows-msvc/c/libloaderapi.cr
index 37a95f3fa089..5612233553d9 100644
--- a/src/lib_c/x86_64-windows-msvc/c/libloaderapi.cr
+++ b/src/lib_c/x86_64-windows-msvc/c/libloaderapi.cr
@@ -9,6 +9,9 @@ lib LibC
   fun LoadLibraryExW(lpLibFileName : LPWSTR, hFile : HANDLE, dwFlags : DWORD) : HMODULE
   fun FreeLibrary(hLibModule : HMODULE) : BOOL
 
+  GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x00000002
+  GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS       = 0x00000004
+
   fun GetModuleHandleExW(dwFlags : DWORD, lpModuleName : LPWSTR, phModule : HMODULE*) : BOOL
 
   fun GetProcAddress(hModule : HMODULE, lpProcName : LPSTR) : FARPROC
diff --git a/src/lib_c/x86_64-windows-msvc/c/winnt.cr b/src/lib_c/x86_64-windows-msvc/c/winnt.cr
index 1db4b2def700..e9aecc01e033 100644
--- a/src/lib_c/x86_64-windows-msvc/c/winnt.cr
+++ b/src/lib_c/x86_64-windows-msvc/c/winnt.cr
@@ -392,11 +392,65 @@ lib LibC
     optionalHeader : IMAGE_OPTIONAL_HEADER64
   end
 
+  IMAGE_DIRECTORY_ENTRY_EXPORT =  0
+  IMAGE_DIRECTORY_ENTRY_IMPORT =  1
+  IMAGE_DIRECTORY_ENTRY_IAT    = 12
+
+  struct IMAGE_SECTION_HEADER
+    name : BYTE[8]
+    virtualSize : DWORD
+    virtualAddress : DWORD
+    sizeOfRawData : DWORD
+    pointerToRawData : DWORD
+    pointerToRelocations : DWORD
+    pointerToLinenumbers : DWORD
+    numberOfRelocations : WORD
+    numberOfLinenumbers : WORD
+    characteristics : DWORD
+  end
+
+  struct IMAGE_EXPORT_DIRECTORY
+    characteristics : DWORD
+    timeDateStamp : DWORD
+    majorVersion : WORD
+    minorVersion : WORD
+    name : DWORD
+    base : DWORD
+    numberOfFunctions : DWORD
+    numberOfNames : DWORD
+    addressOfFunctions : DWORD
+    addressOfNames : DWORD
+    addressOfNameOrdinals : DWORD
+  end
+
   struct IMAGE_IMPORT_BY_NAME
     hint : WORD
     name : CHAR[1]
   end
 
+  struct IMAGE_SYMBOL_n_name
+    short : DWORD
+    long : DWORD
+  end
+
+  union IMAGE_SYMBOL_n
+    shortName : BYTE[8]
+    name : IMAGE_SYMBOL_n_name
+  end
+
+  IMAGE_SYM_CLASS_EXTERNAL = 2
+  IMAGE_SYM_CLASS_STATIC   = 3
+
+  @[Packed]
+  struct IMAGE_SYMBOL
+    n : IMAGE_SYMBOL_n
+    value : DWORD
+    sectionNumber : Short
+    type : WORD
+    storageClass : BYTE
+    numberOfAuxSymbols : BYTE
+  end
+
   union IMAGE_THUNK_DATA64_u1
     forwarderString : ULongLong
     function : ULongLong
diff --git a/src/raise.cr b/src/raise.cr
index a8e06a3c3930..0c9563495a94 100644
--- a/src/raise.cr
+++ b/src/raise.cr
@@ -181,7 +181,7 @@ end
     0u64
   end
 {% else %}
-  {% mingw = flag?(:windows) && flag?(:gnu) %}
+  {% mingw = flag?(:win32) && flag?(:gnu) %}
   fun {{ mingw ? "__crystal_personality_imp".id : "__crystal_personality".id }}(
     version : Int32, actions : LibUnwind::Action, exception_class : UInt64, exception_object : LibUnwind::Exception*, context : Void*,
   ) : LibUnwind::ReasonCode

From 24fc1a91ac5c9057a1de24090a40d7badf9f81c8 Mon Sep 17 00:00:00 2001
From: Quinton Miller <nicetas.c@gmail.com>
Date: Thu, 24 Oct 2024 18:30:54 +0800
Subject: [PATCH 7/7] Support OpenSSL on MSYS2 (#15111)

---
 src/openssl/lib_crypto.cr | 12 +++++++-----
 src/openssl/lib_ssl.cr    | 12 +++++++-----
 2 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/src/openssl/lib_crypto.cr b/src/openssl/lib_crypto.cr
index 8d450b28ff17..fecc69ad44fc 100644
--- a/src/openssl/lib_crypto.cr
+++ b/src/openssl/lib_crypto.cr
@@ -1,6 +1,6 @@
 {% begin %}
   lib LibCrypto
-    {% if flag?(:win32) %}
+    {% if flag?(:msvc) %}
       {% from_libressl = false %}
       {% ssl_version = nil %}
       {% for dir in Crystal::LIBRARY_PATH.split(Crystal::System::Process::HOST_PATH_DELIMITER) %}
@@ -13,10 +13,12 @@
       {% end %}
       {% ssl_version ||= "0.0.0" %}
     {% else %}
-      {% from_libressl = (`hash pkg-config 2> /dev/null || printf %s false` != "false") &&
-                         (`test -f $(pkg-config --silence-errors --variable=includedir libcrypto)/openssl/opensslv.h || printf %s false` != "false") &&
-                         (`printf "#include <openssl/opensslv.h>\nLIBRESSL_VERSION_NUMBER" | ${CC:-cc} $(pkg-config --cflags --silence-errors libcrypto || true) -E -`.chomp.split('\n').last != "LIBRESSL_VERSION_NUMBER") %}
-      {% ssl_version = `hash pkg-config 2> /dev/null && pkg-config --silence-errors --modversion libcrypto || printf %s 0.0.0`.split.last.gsub(/[^0-9.]/, "") %}
+      # these have to be wrapped in `sh -c` since for MinGW-w64 the compiler
+      # passes the command string to `LibC.CreateProcessW`
+      {% from_libressl = (`sh -c 'hash pkg-config 2> /dev/null || printf %s false'` != "false") &&
+                         (`sh -c 'test -f $(pkg-config --silence-errors --variable=includedir libcrypto)/openssl/opensslv.h || printf %s false'` != "false") &&
+                         (`sh -c 'printf "#include <openssl/opensslv.h>\nLIBRESSL_VERSION_NUMBER" | ${CC:-cc} $(pkg-config --cflags --silence-errors libcrypto || true) -E -'`.chomp.split('\n').last != "LIBRESSL_VERSION_NUMBER") %}
+      {% ssl_version = `sh -c 'hash pkg-config 2> /dev/null && pkg-config --silence-errors --modversion libcrypto || printf %s 0.0.0'`.split.last.gsub(/[^0-9.]/, "") %}
     {% end %}
 
     {% if from_libressl %}
diff --git a/src/openssl/lib_ssl.cr b/src/openssl/lib_ssl.cr
index 6adb3f172a3b..4e7e2def549c 100644
--- a/src/openssl/lib_ssl.cr
+++ b/src/openssl/lib_ssl.cr
@@ -6,7 +6,7 @@ require "./lib_crypto"
 
 {% begin %}
   lib LibSSL
-    {% if flag?(:win32) %}
+    {% if flag?(:msvc) %}
       {% from_libressl = false %}
       {% ssl_version = nil %}
       {% for dir in Crystal::LIBRARY_PATH.split(Crystal::System::Process::HOST_PATH_DELIMITER) %}
@@ -19,10 +19,12 @@ require "./lib_crypto"
       {% end %}
       {% ssl_version ||= "0.0.0" %}
     {% else %}
-      {% from_libressl = (`hash pkg-config 2> /dev/null || printf %s false` != "false") &&
-                         (`test -f $(pkg-config --silence-errors --variable=includedir libssl)/openssl/opensslv.h || printf %s false` != "false") &&
-                         (`printf "#include <openssl/opensslv.h>\nLIBRESSL_VERSION_NUMBER" | ${CC:-cc} $(pkg-config --cflags --silence-errors libssl || true) -E -`.chomp.split('\n').last != "LIBRESSL_VERSION_NUMBER") %}
-      {% ssl_version = `hash pkg-config 2> /dev/null && pkg-config --silence-errors --modversion libssl || printf %s 0.0.0`.split.last.gsub(/[^0-9.]/, "") %}
+      # these have to be wrapped in `sh -c` since for MinGW-w64 the compiler
+      # passes the command string to `LibC.CreateProcessW`
+      {% from_libressl = (`sh -c 'hash pkg-config 2> /dev/null || printf %s false'` != "false") &&
+                         (`sh -c 'test -f $(pkg-config --silence-errors --variable=includedir libssl)/openssl/opensslv.h || printf %s false'` != "false") &&
+                         (`sh -c 'printf "#include <openssl/opensslv.h>\nLIBRESSL_VERSION_NUMBER" | ${CC:-cc} $(pkg-config --cflags --silence-errors libssl || true) -E -'`.chomp.split('\n').last != "LIBRESSL_VERSION_NUMBER") %}
+      {% ssl_version = `sh -c 'hash pkg-config 2> /dev/null && pkg-config --silence-errors --modversion libssl || printf %s 0.0.0'`.split.last.gsub(/[^0-9.]/, "") %}
     {% end %}
 
     {% if from_libressl %}