Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable the interpreter on Windows #14964

Merged
Merged
37 changes: 36 additions & 1 deletion .github/workflows/win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}

env:
SPEC_SPLIT_DOTS: 160
CI_LLVM_VERSION: "18.1.1"

jobs:
Expand Down Expand Up @@ -266,13 +267,47 @@ jobs:
run: make -f Makefile.win samples

x86_64-windows-release:
if: github.repository_owner == 'crystal-lang' && (startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/ci/'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: This change means building a compiler in release mode on every workflow run. I'm wondering if it makes sense to keep the non-release build around then? We could build just a single compiler (in release mode) and use that for x86_64-windows-test as well.

Maybe we should do this as a follow-up though.

needs: [x86_64-windows-libs, x86_64-windows-dlls, x86_64-windows-llvm-libs, x86_64-windows-llvm-dlls]
uses: ./.github/workflows/win_build_portable.yml
with:
release: true
llvm_version: "18.1.1"

x86_64-windows-test-interpreter:
runs-on: windows-2022
needs: [x86_64-windows-release]
steps:
- name: Disable CRLF line ending substitution
run: |
git config --global core.autocrlf false

- name: Download Crystal source
uses: actions/checkout@v4

- name: Download Crystal executable
uses: actions/download-artifact@v4
with:
name: crystal-release
path: build

- name: Restore LLVM
uses: actions/cache/restore@v4
with:
path: llvm
key: llvm-libs-${{ env.CI_LLVM_VERSION }}-msvc
fail-on-cache-miss: true

- name: Set up environment
run: |
Add-Content $env:GITHUB_PATH "$(pwd)\build"
Add-Content $env:GITHUB_ENV "CRYSTAL_SPEC_COMPILER_BIN=$(pwd)\build\crystal.exe"

- name: Run stdlib specs with interpreter
run: bin\crystal i spec\std_spec.cr

- name: Run primitives specs with interpreter
run: bin\crystal i spec\primitives_spec.cr

x86_64-windows-installer:
if: github.repository_owner == 'crystal-lang' && (startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/ci/'))
runs-on: windows-2022
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/win_build_portable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ jobs:
- name: Build Crystal
run: |
bin/crystal.bat env
make -f Makefile.win -B ${{ inputs.release && 'release=1' || '' }}
make -f Makefile.win -B ${{ inputs.release && 'release=1' || '' }} interpreter=1

- name: Download shards release
uses: actions/checkout@v4
Expand Down
6 changes: 6 additions & 0 deletions spec/std/http/client/client_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ require "http/server"
require "http/log"
require "log/spec"

# TODO: Windows networking in the interpreter requires #12495
{% if flag?(:interpreted) && flag?(:win32) %}
pending HTTP::Client
{% skip_file %}
{% end %}

private def test_server(host, port, read_time = 0.seconds, content_type = "text/plain", write_response = true, &)
server = TCPServer.new(host, port)
begin
Expand Down
6 changes: 6 additions & 0 deletions spec/std/http/server/server_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ require "http/client"
require "../../../support/ssl"
require "../../../support/channel"

# TODO: Windows networking in the interpreter requires #12495
{% if flag?(:interpreted) && flag?(:win32) %}
pending HTTP::Server
{% skip_file %}
{% end %}

# TODO: replace with `HTTP::Client.get` once it supports connecting to Unix socket (#2735)
private def unix_request(path)
UNIXSocket.open(path) do |io|
Expand Down
6 changes: 6 additions & 0 deletions spec/std/http/web_socket_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ require "../../support/fibers"
require "../../support/ssl"
require "../socket/spec_helper.cr"

# TODO: Windows networking in the interpreter requires #12495
{% if flag?(:interpreted) && flag?(:win32) %}
pending HTTP::WebSocket
{% skip_file %}
{% end %}

private def assert_text_packet(packet, size, final = false)
assert_packet packet, HTTP::WebSocket::Protocol::Opcode::TEXT, size, final: final
end
Expand Down
33 changes: 18 additions & 15 deletions spec/std/io/io_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -816,23 +816,26 @@ describe IO do
io.gets_to_end.should eq("\r\nFoo\nBar")
end

it "gets ascii from socket (#9056)" do
server = TCPServer.new "localhost", 0
sock = TCPSocket.new "localhost", server.local_address.port
begin
sock.set_encoding("ascii")
spawn do
client = server.accept
message = client.gets
client << "#{message}\n"
# TODO: Windows networking in the interpreter requires #12495
{% unless flag?(:interpreted) || flag?(:win32) %}
it "gets ascii from socket (#9056)" do
server = TCPServer.new "localhost", 0
sock = TCPSocket.new "localhost", server.local_address.port
begin
sock.set_encoding("ascii")
spawn do
client = server.accept
message = client.gets
client << "#{message}\n"
end
sock << "K\n"
sock.gets.should eq("K")
ensure
server.close
sock.close
end
sock << "K\n"
sock.gets.should eq("K")
ensure
server.close
sock.close
end
end
{% end %}
end

describe "encode" do
Expand Down
6 changes: 6 additions & 0 deletions spec/std/oauth2/client_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ require "oauth2"
require "http/server"
require "../http/spec_helper"

# TODO: Windows networking in the interpreter requires #12495
{% if flag?(:interpreted) && flag?(:win32) %}
pending OAuth2::Client
{% skip_file %}
{% end %}

describe OAuth2::Client do
describe "authorization uri" do
it "gets with default endpoint" do
Expand Down
6 changes: 6 additions & 0 deletions spec/std/openssl/ssl/server_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ require "socket"
require "../../spec_helper"
require "../../../support/ssl"

# TODO: Windows networking in the interpreter requires #12495
{% if flag?(:interpreted) && flag?(:win32) %}
pending OpenSSL::SSL::Server
{% skip_file %}
{% end %}

describe OpenSSL::SSL::Server do
it "sync_close" do
TCPServer.open(0) do |tcp_server|
Expand Down
6 changes: 6 additions & 0 deletions spec/std/openssl/ssl/socket_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ require "../../spec_helper"
require "../../socket/spec_helper"
require "../../../support/ssl"

# TODO: Windows networking in the interpreter requires #12495
{% if flag?(:interpreted) && flag?(:win32) %}
pending OpenSSL::SSL::Socket
{% skip_file %}
{% end %}

describe OpenSSL::SSL::Socket do
describe OpenSSL::SSL::Socket::Server do
it "auto accept client by default" do
Expand Down
7 changes: 6 additions & 1 deletion spec/std/process_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ private def newline
end

# interpreted code doesn't receive SIGCHLD for `#wait` to work (#12241)
pending_interpreted describe: Process do
{% if flag?(:interpreted) && !flag?(:win32) %}
pending Process
{% skip_file %}
{% end %}

describe Process do
describe ".new" do
it "raises if command doesn't exist" do
expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
Expand Down
6 changes: 6 additions & 0 deletions spec/std/socket/socket_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ require "./spec_helper"
require "../../support/tempfile"
require "../../support/win32"

# TODO: Windows networking in the interpreter requires #12495
{% if flag?(:interpreted) && flag?(:win32) %}
pending Socket
{% skip_file %}
{% end %}

describe Socket, tags: "network" do
describe ".unix" do
it "creates a unix socket" do
Expand Down
6 changes: 6 additions & 0 deletions spec/std/socket/tcp_socket_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
require "./spec_helper"
require "../../support/win32"

# TODO: Windows networking in the interpreter requires #12495
{% if flag?(:interpreted) && flag?(:win32) %}
pending TCPSocket
{% skip_file %}
{% end %}

describe TCPSocket, tags: "network" do
describe "#connect" do
each_ip_family do |family, address|
Expand Down
6 changes: 6 additions & 0 deletions spec/std/socket/unix_server_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ require "../../support/fibers"
require "../../support/channel"
require "../../support/tempfile"

# TODO: Windows networking in the interpreter requires #12495
{% if flag?(:interpreted) && flag?(:win32) %}
pending UNIXServer
{% skip_file %}
{% end %}

describe UNIXServer do
describe ".new" do
it "raises when path is too long" do
Expand Down
6 changes: 6 additions & 0 deletions spec/std/socket/unix_socket_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ require "spec"
require "socket"
require "../../support/tempfile"

# TODO: Windows networking in the interpreter requires #12495
{% if flag?(:interpreted) && flag?(:win32) %}
pending UNIXSocket
{% skip_file %}
{% end %}

describe UNIXSocket do
it "raises when path is too long" do
with_tempfile("unix_socket-too_long-#{("a" * 2048)}.sock") do |path|
Expand Down
1 change: 1 addition & 0 deletions spec/std/uuid_spec.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "spec"
require "uuid"
require "spec/helpers/string"
require "../support/wasm32"

describe "UUID" do
describe "#==" do
Expand Down
37 changes: 28 additions & 9 deletions src/compiler/crystal/loader/msvc.cr
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,25 @@ class Crystal::Loader
end

def load_file?(path : String | ::Path) : Bool
# API sets shouldn't be linked directly from linker flags, but just in case
if api_set?(path)
return load_dll?(path.to_s)
end

return false unless File.file?(path)

# On Windows, each `.lib` import library may reference any number of `.dll`
# files, whose base names may not match the library's. Thus it is necessary
# to extract this information from the library archive itself.
System::LibraryArchive.imported_dlls(path).each do |dll|
dll_full_path = @dll_search_paths.try &.each do |search_path|
full_path = File.join(search_path, dll)
break full_path if File.file?(full_path)
System::LibraryArchive.imported_dlls(path).all? do |dll|
# API set names do not refer to physical filenames despite ending with
# `.dll`, and therefore should not use a path search:
# https://learn.microsoft.com/en-us/cpp/windows/universal-crt-deployment?view=msvc-170#local-deployment
unless api_set?(dll)
dll_full_path = @dll_search_paths.try &.each do |search_path|
full_path = File.join(search_path, dll)
break full_path if File.file?(full_path)
end
end
dll = dll_full_path || dll

Expand All @@ -152,13 +162,16 @@ class Crystal::Loader
#
# Note that the compiler's directory and PATH are effectively searched
# twice when coming from the interpreter
handle = open_library(dll)
return false unless handle

@handles << handle
@loaded_libraries << (module_filename(handle) || dll)
load_dll?(dll)
end
end

private def load_dll?(dll)
handle = open_library(dll)
return false unless handle

@handles << handle
@loaded_libraries << (module_filename(handle) || dll)
true
end

Expand Down Expand Up @@ -190,6 +203,12 @@ class Crystal::Loader
@handles.clear
end

# Returns whether *dll* names an API set according to:
# https://learn.microsoft.com/en-us/windows/win32/apiindex/windows-apisets#api-set-contract-names
private def api_set?(dll)
dll.to_s.matches?(/^(?:api-|ext-)[a-zA-Z0-9-]*l\d+-\d+-\d+\.dll$/)
end

private def module_filename(handle)
Crystal::System.retry_wstr_buffer do |buffer, small_buf|
len = LibC.GetModuleFileNameW(handle, buffer, buffer.size)
Expand Down
13 changes: 13 additions & 0 deletions src/kernel.cr
Original file line number Diff line number Diff line change
Expand Up @@ -616,3 +616,16 @@ end
Crystal::System::Signal.setup_default_handlers
{% end %}
{% end %}

# This is a temporary workaround to ensure there is always something in the IOCP
# event loop being awaited, since both the interrupt loop and the fiber stack
# pool collector are disabled in interpreted code. Without this, asynchronous
# code that bypasses `Crystal::IOCP::OverlappedOperation` does not currently
# work, see https://github.com/crystal-lang/crystal/pull/14949#issuecomment-2328314463
{% if flag?(:interpreted) && flag?(:win32) %}
spawn(name: "Interpreter idle loop") do
while true
sleep 1.day
end
end
{% end %}
Loading