diff --git a/.circleci/config.yml b/.circleci/config.yml index 574d390f3bc3..baf9b41a1be3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -285,7 +285,7 @@ jobs: dist_darwin: macos: - xcode: 13.4.1 + xcode: 15.3.0 shell: /bin/bash --login -eo pipefail steps: - restore_cache: diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 79c3f143d303..eb5874a2687a 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -20,6 +20,7 @@ jobs: DOCKER_TEST_PREFIX: crystallang/crystal:${{ matrix.crystal_bootstrap_version }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: crystal_bootstrap_version: [1.7.3, 1.8.2, 1.9.2, 1.10.1, 1.11.2, 1.12.2, 1.13.3, 1.14.0] flags: [""] diff --git a/.github/workflows/mingw-w64.yml b/.github/workflows/mingw-w64.yml index 10841a325bf5..050f8800b520 100644 --- a/.github/workflows/mingw-w64.yml +++ b/.github/workflows/mingw-w64.yml @@ -18,12 +18,13 @@ jobs: - name: Download Crystal source uses: actions/checkout@v4 - - name: Install LLVM 18 + - name: Install LLVM run: | + _llvm_major=$(wget -qO- https://raw.githubusercontent.com/msys2/MINGW-packages/refs/heads/master/mingw-w64-llvm/PKGBUILD | grep '_version=' | sed -E 's/_version=([0-9]+).*/\1/') 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 + sudo apt-add-repository -y deb http://apt.llvm.org/noble/ llvm-toolchain-noble-${_llvm_major} main + sudo apt install -y llvm-${_llvm_major}-dev - name: Install Crystal uses: crystal-lang/install-crystal@v1 @@ -39,12 +40,6 @@ jobs: 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] @@ -56,6 +51,7 @@ jobs: msystem: UCRT64 update: true install: >- + make mingw-w64-ucrt-x86_64-pkgconf mingw-w64-ucrt-x86_64-cc mingw-w64-ucrt-x86_64-gc @@ -65,34 +61,37 @@ jobs: mingw-w64-ucrt-x86_64-llvm mingw-w64-ucrt-x86_64-libffi + - name: Disable CRLF line ending substitution + run: | + git config --global core.autocrlf false + + - name: Download Crystal source + uses: actions/checkout@v4 + - 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 \ + mkdir .build + cc crystal.obj -o .build/crystal.exe -municode \ $(pkg-config bdw-gc libpcre2-8 iconv zlib libffi --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/ + -lole32 -lWS2_32 -Wl,--stack,0x800000 - - name: Upload Crystal + - name: Package Crystal + shell: msys2 {0} + run: | + make install install_dlls deref_symlinks=1 PREFIX="$(pwd)/crystal" + + - name: Upload Crystal executable uses: actions/upload-artifact@v4 with: name: x86_64-mingw-w64-crystal - path: | - bin/ - share/ + path: crystal x86_64-mingw-w64-test: runs-on: windows-2022 diff --git a/spec/std/crystal/evented/arena_spec.cr b/spec/std/crystal/evented/arena_spec.cr new file mode 100644 index 000000000000..c25bb9ec1adc --- /dev/null +++ b/spec/std/crystal/evented/arena_spec.cr @@ -0,0 +1,226 @@ +{% skip_file unless Crystal.has_constant?(:Evented) %} + +require "spec" + +describe Crystal::Evented::Arena do + describe "#allocate_at?" do + it "yields block when not allocated" do + arena = Crystal::Evented::Arena(Int32).new(32) + pointer = nil + index = nil + called = 0 + + ret = arena.allocate_at?(0) do |ptr, idx| + pointer = ptr + index = idx + called += 1 + end + ret.should eq(index) + called.should eq(1) + + ret = arena.allocate_at?(0) { called += 1 } + ret.should be_nil + called.should eq(1) + + pointer.should_not be_nil + index.should_not be_nil + + arena.get(index.not_nil!) do |ptr| + ptr.should eq(pointer) + end + end + + it "allocates up to capacity" do + arena = Crystal::Evented::Arena(Int32).new(32) + indexes = [] of Crystal::Evented::Arena::Index + + indexes = 32.times.map do |i| + arena.allocate_at?(i) { |ptr, _| ptr.value = i } + end.to_a + + indexes.size.should eq(32) + + indexes.each do |index| + arena.get(index.not_nil!) do |pointer| + pointer.should eq(pointer) + pointer.value.should eq(index.not_nil!.index) + end + end + end + + it "checks bounds" do + arena = Crystal::Evented::Arena(Int32).new(32) + expect_raises(IndexError) { arena.allocate_at?(-1) { } } + expect_raises(IndexError) { arena.allocate_at?(33) { } } + end + end + + describe "#get" do + it "returns previously allocated object" do + arena = Crystal::Evented::Arena(Int32).new(32) + pointer = nil + + index = arena.allocate_at(30) do |ptr| + pointer = ptr + ptr.value = 654321 + end + called = 0 + + 2.times do + arena.get(index.not_nil!) do |ptr| + ptr.should eq(pointer) + ptr.value.should eq(654321) + called += 1 + end + end + called.should eq(2) + end + + it "can't access unallocated object" do + arena = Crystal::Evented::Arena(Int32).new(32) + + expect_raises(RuntimeError) do + arena.get(Crystal::Evented::Arena::Index.new(10, 0)) { } + end + end + + it "checks generation" do + arena = Crystal::Evented::Arena(Int32).new(32) + called = 0 + + index1 = arena.allocate_at(2) { called += 1 } + called.should eq(1) + + arena.free(index1) { } + expect_raises(RuntimeError) { arena.get(index1) { } } + + index2 = arena.allocate_at(2) { called += 1 } + called.should eq(2) + expect_raises(RuntimeError) { arena.get(index1) { } } + + arena.get(index2) { } + end + + it "checks out of bounds" do + arena = Crystal::Evented::Arena(Int32).new(32) + expect_raises(IndexError) { arena.get(Crystal::Evented::Arena::Index.new(-1, 0)) { } } + expect_raises(IndexError) { arena.get(Crystal::Evented::Arena::Index.new(33, 0)) { } } + end + end + + describe "#get?" do + it "returns previously allocated object" do + arena = Crystal::Evented::Arena(Int32).new(32) + pointer = nil + + index = arena.allocate_at(30) do |ptr| + pointer = ptr + ptr.value = 654321 + end + + called = 0 + 2.times do + ret = arena.get?(index) do |ptr| + ptr.should eq(pointer) + ptr.not_nil!.value.should eq(654321) + called += 1 + end + ret.should be_true + end + called.should eq(2) + end + + it "can't access unallocated index" do + arena = Crystal::Evented::Arena(Int32).new(32) + + called = 0 + ret = arena.get?(Crystal::Evented::Arena::Index.new(10, 0)) { called += 1 } + ret.should be_false + called.should eq(0) + end + + it "checks generation" do + arena = Crystal::Evented::Arena(Int32).new(32) + called = 0 + + old_index = arena.allocate_at(2) { } + arena.free(old_index) { } + + # not accessible after free: + ret = arena.get?(old_index) { called += 1 } + ret.should be_false + called.should eq(0) + + # can be reallocated: + new_index = arena.allocate_at(2) { } + + # still not accessible after reallocate: + ret = arena.get?(old_index) { called += 1 } + ret.should be_false + called.should eq(0) + + # accessible after reallocate (new index): + ret = arena.get?(new_index) { called += 1 } + ret.should be_true + called.should eq(1) + end + + it "checks out of bounds" do + arena = Crystal::Evented::Arena(Int32).new(32) + called = 0 + + arena.get?(Crystal::Evented::Arena::Index.new(-1, 0)) { called += 1 }.should be_false + arena.get?(Crystal::Evented::Arena::Index.new(33, 0)) { called += 1 }.should be_false + + called.should eq(0) + end + end + + describe "#free" do + it "deallocates the object" do + arena = Crystal::Evented::Arena(Int32).new(32) + + index1 = arena.allocate_at(3) { |ptr| ptr.value = 123 } + arena.free(index1) { } + + index2 = arena.allocate_at(3) { } + index2.should_not eq(index1) + + value = nil + arena.get(index2) { |ptr| value = ptr.value } + value.should eq(0) + end + + it "checks generation" do + arena = Crystal::Evented::Arena(Int32).new(32) + + called = 0 + old_index = arena.allocate_at(1) { } + + # can free: + arena.free(old_index) { called += 1 } + called.should eq(1) + + # can reallocate: + new_index = arena.allocate_at(1) { } + + # can't free with invalid index: + arena.free(old_index) { called += 1 } + called.should eq(1) + + # but new index can: + arena.free(new_index) { called += 1 } + called.should eq(2) + end + + it "checks out of bounds" do + arena = Crystal::Evented::Arena(Int32).new(32) + called = 0 + + arena.free(Crystal::Evented::Arena::Index.new(-1, 0)) { called += 1 } + arena.free(Crystal::Evented::Arena::Index.new(33, 0)) { called += 1 } + + called.should eq(0) + end + end +end diff --git a/spec/std/crystal/evented/poll_descriptor_spec.cr b/spec/std/crystal/evented/poll_descriptor_spec.cr new file mode 100644 index 000000000000..d50ecd1036b9 --- /dev/null +++ b/spec/std/crystal/evented/poll_descriptor_spec.cr @@ -0,0 +1,100 @@ +{% skip_file unless Crystal.has_constant?(:Evented) %} + +require "spec" + +class Crystal::Evented::FakeLoop < Crystal::Evented::EventLoop + getter operations = [] of {Symbol, Int32, Crystal::Evented::Arena::Index | Bool} + + private def system_run(blocking : Bool) : Nil + end + + private def interrupt : Nil + end + + protected def system_add(fd : Int32, index : Arena::Index) : Nil + operations << {:add, fd, index} + end + + protected def system_del(fd : Int32, closing = true) : Nil + operations << {:del, fd, closing} + end + + protected def system_del(fd : Int32, closing = true, &) : Nil + operations << {:del, fd, closing} + end + + private def system_set_timer(time : Time::Span?) : Nil + end +end + +describe Crystal::Evented::Waiters do + describe "#take_ownership" do + it "associates a poll descriptor to an evloop instance" do + fd = Int32::MAX + pd = Crystal::Evented::PollDescriptor.new + index = Crystal::Evented::Arena::Index.new(fd, 0) + evloop = Crystal::Evented::FakeLoop.new + + pd.take_ownership(evloop, fd, index) + pd.@event_loop.should be(evloop) + + evloop.operations.should eq([ + {:add, fd, index}, + ]) + end + + it "moves a poll descriptor to another evloop instance" do + fd = Int32::MAX + pd = Crystal::Evented::PollDescriptor.new + index = Crystal::Evented::Arena::Index.new(fd, 0) + + evloop1 = Crystal::Evented::FakeLoop.new + evloop2 = Crystal::Evented::FakeLoop.new + + pd.take_ownership(evloop1, fd, index) + pd.take_ownership(evloop2, fd, index) + + pd.@event_loop.should be(evloop2) + + evloop1.operations.should eq([ + {:add, fd, index}, + {:del, fd, false}, + ]) + evloop2.operations.should eq([ + {:add, fd, index}, + ]) + end + + it "can't move to the current evloop" do + fd = Int32::MAX + pd = Crystal::Evented::PollDescriptor.new + index = Crystal::Evented::Arena::Index.new(fd, 0) + + evloop = Crystal::Evented::FakeLoop.new + + pd.take_ownership(evloop, fd, index) + expect_raises(Exception) { pd.take_ownership(evloop, fd, index) } + end + + it "can't move with pending waiters" do + fd = Int32::MAX + pd = Crystal::Evented::PollDescriptor.new + index = Crystal::Evented::Arena::Index.new(fd, 0) + event = Crystal::Evented::Event.new(:io_read, Fiber.current) + + evloop1 = Crystal::Evented::FakeLoop.new + pd.take_ownership(evloop1, fd, index) + pd.@readers.add(pointerof(event)) + + evloop2 = Crystal::Evented::FakeLoop.new + expect_raises(RuntimeError) { pd.take_ownership(evloop2, fd, index) } + + pd.@event_loop.should be(evloop1) + + evloop1.operations.should eq([ + {:add, fd, index}, + ]) + evloop2.operations.should be_empty + end + end +end diff --git a/spec/std/crystal/evented/timers_spec.cr b/spec/std/crystal/evented/timers_spec.cr new file mode 100644 index 000000000000..d40917910d1d --- /dev/null +++ b/spec/std/crystal/evented/timers_spec.cr @@ -0,0 +1,100 @@ +{% skip_file unless Crystal.has_constant?(:Evented) %} + +require "spec" + +describe Crystal::Evented::Timers do + it "#empty?" do + timers = Crystal::Evented::Timers.new + timers.empty?.should be_true + + event = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 7.seconds) + timers.add(pointerof(event)) + timers.empty?.should be_false + end + + it "#next_ready?" do + # empty + timers = Crystal::Evented::Timers.new + timers.next_ready?.should be_nil + + # with events + event = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 5.seconds) + timers.add(pointerof(event)) + timers.next_ready?.should eq(event.wake_at?) + end + + it "#dequeue_ready" do + timers = Crystal::Evented::Timers.new + + event1 = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 0.seconds) + event2 = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 0.seconds) + event3 = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 1.minute) + + # empty + called = 0 + timers.dequeue_ready { called += 1 } + called.should eq(0) + + # add events in non chronological order + timers = Crystal::Evented::Timers.new + timers.add(pointerof(event1)) + timers.add(pointerof(event3)) + timers.add(pointerof(event2)) + + events = [] of Crystal::Evented::Event* + timers.dequeue_ready { |event| events << event } + + events.should eq([ + pointerof(event1), + pointerof(event2), + ]) + timers.empty?.should be_false + end + + it "#add" do + timers = Crystal::Evented::Timers.new + + event0 = Crystal::Evented::Event.new(:sleep, Fiber.current) + event1 = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 0.seconds) + event2 = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 2.minutes) + event3 = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 1.minute) + + # add events in non chronological order + timers.add(pointerof(event1)).should be_true # added to the head (next ready) + timers.add(pointerof(event2)).should be_false + timers.add(pointerof(event3)).should be_false + + event0.wake_at = -1.minute + timers.add(pointerof(event0)).should be_true # added new head (next ready) + + events = [] of Crystal::Evented::Event* + timers.each { |event| events << event } + events.should eq([ + pointerof(event0), + pointerof(event1), + pointerof(event3), + pointerof(event2), + ]) + timers.empty?.should be_false + end + + it "#delete" do + event1 = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 0.seconds) + event2 = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 0.seconds) + event3 = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 1.minute) + event4 = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 4.minutes) + + # add events in non chronological order + timers = Crystal::Evented::Timers.new + timers.add(pointerof(event1)) + timers.add(pointerof(event3)) + timers.add(pointerof(event2)) + + timers.delete(pointerof(event1)).should eq({true, true}) # dequeued+removed head (next ready) + timers.delete(pointerof(event3)).should eq({true, false}) # dequeued + timers.delete(pointerof(event2)).should eq({true, true}) # dequeued+removed new head (next ready) + timers.empty?.should be_true + timers.delete(pointerof(event2)).should eq({false, false}) # not dequeued + timers.delete(pointerof(event4)).should eq({false, false}) # not dequeued + end +end diff --git a/spec/std/crystal/evented/waiters_spec.cr b/spec/std/crystal/evented/waiters_spec.cr new file mode 100644 index 000000000000..91e145f6f811 --- /dev/null +++ b/spec/std/crystal/evented/waiters_spec.cr @@ -0,0 +1,168 @@ +{% skip_file unless Crystal.has_constant?(:Evented) %} + +require "spec" + +describe Crystal::Evented::Waiters do + describe "#add" do + it "adds event to list" do + waiters = Crystal::Evented::Waiters.new + + event = Crystal::Evented::Event.new(:io_read, Fiber.current) + ret = waiters.add(pointerof(event)) + ret.should be_true + end + + it "doesn't add the event when the list is ready (race condition)" do + waiters = Crystal::Evented::Waiters.new + waiters.ready_one { true } + + event = Crystal::Evented::Event.new(:io_read, Fiber.current) + ret = waiters.add(pointerof(event)) + ret.should be_false + waiters.@ready.should be_false + end + + it "doesn't add the event when the list is always ready" do + waiters = Crystal::Evented::Waiters.new + waiters.ready_all { } + + event = Crystal::Evented::Event.new(:io_read, Fiber.current) + ret = waiters.add(pointerof(event)) + ret.should be_false + waiters.@always_ready.should be_true + end + end + + describe "#delete" do + it "removes the event from the list" do + waiters = Crystal::Evented::Waiters.new + event = Crystal::Evented::Event.new(:io_read, Fiber.current) + + waiters.add(pointerof(event)) + waiters.delete(pointerof(event)) + + called = false + waiters.ready_one { called = true } + called.should be_false + end + + it "does nothing when the event isn't in the list" do + waiters = Crystal::Evented::Waiters.new + event = Crystal::Evented::Event.new(:io_read, Fiber.current) + waiters.delete(pointerof(event)) + end + end + + describe "#ready_one" do + it "marks the list as ready when empty (race condition)" do + waiters = Crystal::Evented::Waiters.new + called = false + + waiters.ready_one { called = true } + + called.should be_false + waiters.@ready.should be_true + end + + it "dequeues events in FIFO order" do + waiters = Crystal::Evented::Waiters.new + event1 = Crystal::Evented::Event.new(:io_read, Fiber.current) + event2 = Crystal::Evented::Event.new(:io_read, Fiber.current) + event3 = Crystal::Evented::Event.new(:io_read, Fiber.current) + called = 0 + + waiters.add(pointerof(event1)) + waiters.add(pointerof(event2)) + waiters.add(pointerof(event3)) + + 3.times do + waiters.ready_one do |event| + case called += 1 + when 1 then event.should eq(pointerof(event1)) + when 2 then event.should eq(pointerof(event2)) + when 3 then event.should eq(pointerof(event3)) + end + true + end + end + called.should eq(3) + waiters.@ready.should be_false + + waiters.ready_one do + called += 1 + true + end + called.should eq(3) + waiters.@ready.should be_true + end + + it "dequeues events until the block returns true" do + waiters = Crystal::Evented::Waiters.new + event1 = Crystal::Evented::Event.new(:io_read, Fiber.current) + event2 = Crystal::Evented::Event.new(:io_read, Fiber.current) + event3 = Crystal::Evented::Event.new(:io_read, Fiber.current) + called = 0 + + waiters.add(pointerof(event1)) + waiters.add(pointerof(event2)) + waiters.add(pointerof(event3)) + + waiters.ready_one do |event| + (called += 1) == 2 + end + called.should eq(2) + waiters.@ready.should be_false + end + + it "dequeues events until empty and marks the list as ready" do + waiters = Crystal::Evented::Waiters.new + event1 = Crystal::Evented::Event.new(:io_read, Fiber.current) + event2 = Crystal::Evented::Event.new(:io_read, Fiber.current) + called = 0 + + waiters.add(pointerof(event1)) + waiters.add(pointerof(event2)) + + waiters.ready_one do |event| + called += 1 + false + end + called.should eq(2) + waiters.@ready.should be_true + end + end + + describe "#ready_all" do + it "marks the list as always ready" do + waiters = Crystal::Evented::Waiters.new + called = false + + waiters.ready_all { called = true } + + called.should be_false + waiters.@always_ready.should be_true + end + + it "dequeues all events" do + waiters = Crystal::Evented::Waiters.new + event1 = Crystal::Evented::Event.new(:io_read, Fiber.current) + event2 = Crystal::Evented::Event.new(:io_read, Fiber.current) + event3 = Crystal::Evented::Event.new(:io_read, Fiber.current) + called = 0 + + waiters.add(pointerof(event1)) + waiters.add(pointerof(event2)) + waiters.add(pointerof(event3)) + + waiters.ready_all do |event| + case called += 1 + when 1 then event.should eq(pointerof(event1)) + when 2 then event.should eq(pointerof(event2)) + when 3 then event.should eq(pointerof(event3)) + end + end + called.should eq(3) + waiters.@always_ready.should be_true + end + end +end diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index 3c634d755abf..ce8e76f9a11e 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -18,7 +18,7 @@ private def unix_request(path) end private def unused_port - TCPServer.open(0) do |server| + TCPServer.open(Socket::IPAddress::UNSPECIFIED, 0) do |server| server.local_address.port end end diff --git a/spec/std/openssl/pkcs5_spec.cr b/spec/std/openssl/pkcs5_spec.cr index 70fd351f0bbc..a8261d42c1f6 100644 --- a/spec/std/openssl/pkcs5_spec.cr +++ b/spec/std/openssl/pkcs5_spec.cr @@ -13,7 +13,7 @@ describe OpenSSL::PKCS5 do end end - {% if compare_versions(LibSSL::OPENSSL_VERSION, "1.0.0") >= 0 %} + {% if compare_versions(LibSSL::OPENSSL_VERSION, "1.0.0") >= 0 || LibSSL::LIBRESSL_VERSION != "0.0.0" %} {% if compare_versions(LibSSL::OPENSSL_VERSION, "3.0.0") < 0 %} [ {OpenSSL::Algorithm::MD4, 1, 16, "1857f69412150bca4542581d0f9e7fd1"}, diff --git a/spec/std/openssl/ssl/context_spec.cr b/spec/std/openssl/ssl/context_spec.cr index 74c79411c82a..c37055dcedec 100644 --- a/spec/std/openssl/ssl/context_spec.cr +++ b/spec/std/openssl/ssl/context_spec.cr @@ -32,7 +32,7 @@ describe OpenSSL::SSL::Context do (context.options & OpenSSL::SSL::Options::NO_SESSION_RESUMPTION_ON_RENEGOTIATION).should eq(OpenSSL::SSL::Options::NO_SESSION_RESUMPTION_ON_RENEGOTIATION) (context.options & OpenSSL::SSL::Options::SINGLE_ECDH_USE).should eq(OpenSSL::SSL::Options::SINGLE_ECDH_USE) (context.options & OpenSSL::SSL::Options::SINGLE_DH_USE).should eq(OpenSSL::SSL::Options::SINGLE_DH_USE) - {% if compare_versions(LibSSL::OPENSSL_VERSION, "1.1.0") >= 0 %} + {% if LibSSL::Options.has_constant?(:NO_RENEGOTIATION) %} (context.options & OpenSSL::SSL::Options::NO_RENEGOTIATION).should eq(OpenSSL::SSL::Options::NO_RENEGOTIATION) {% end %} @@ -128,12 +128,12 @@ describe OpenSSL::SSL::Context do context = OpenSSL::SSL::Context::Client.new level = context.security_level context.security_level = level + 1 - # SSL_CTX_get_security_level is not supported by libressl - {% if LibSSL::OPENSSL_VERSION != "0.0.0" %} + + if LibSSL.responds_to?(:ssl_ctx_set_security_level) context.security_level.should eq(level + 1) - {% else %} + else context.security_level.should eq 0 - {% end %} + end end it "adds temporary ecdh curve (P-256)" do @@ -194,12 +194,12 @@ describe OpenSSL::SSL::Context do context.verify_mode.should eq(OpenSSL::SSL::VerifyMode::PEER) end - {% if compare_versions(LibSSL::OPENSSL_VERSION, "1.0.2") >= 0 %} + if LibSSL.responds_to?(:ssl_ctx_set_alpn_protos) it "alpn_protocol=" do context = OpenSSL::SSL::Context::Client.insecure context.alpn_protocol = "h2" end - {% end %} + end it "calls #finalize on insecure client context" do assert_finalizes("insecure_client_ctx") { OpenSSL::SSL::Context::Client.insecure } diff --git a/spec/std/openssl/ssl/server_spec.cr b/spec/std/openssl/ssl/server_spec.cr index 8618ed780a50..d2cc41efe88b 100644 --- a/spec/std/openssl/ssl/server_spec.cr +++ b/spec/std/openssl/ssl/server_spec.cr @@ -11,7 +11,7 @@ require "../../../support/ssl" describe OpenSSL::SSL::Server do it "sync_close" do - TCPServer.open(0) do |tcp_server| + TCPServer.open(Socket::IPAddress::UNSPECIFIED, 0) do |tcp_server| context = OpenSSL::SSL::Context::Server.new ssl_server = OpenSSL::SSL::Server.new(tcp_server, context) @@ -22,7 +22,7 @@ describe OpenSSL::SSL::Server do end it "don't sync_close" do - TCPServer.open(0) do |tcp_server| + TCPServer.open(Socket::IPAddress::UNSPECIFIED, 0) do |tcp_server| context = OpenSSL::SSL::Context::Server.new ssl_server = OpenSSL::SSL::Server.new(tcp_server, context, sync_close: false) ssl_server.context.should eq context @@ -35,7 +35,7 @@ describe OpenSSL::SSL::Server do it ".new" do context = OpenSSL::SSL::Context::Server.new - TCPServer.open(0) do |tcp_server| + TCPServer.open(Socket::IPAddress::UNSPECIFIED, 0) do |tcp_server| ssl_server = OpenSSL::SSL::Server.new tcp_server, context, sync_close: false ssl_server.context.should eq context @@ -46,7 +46,7 @@ describe OpenSSL::SSL::Server do it ".open" do context = OpenSSL::SSL::Context::Server.new - TCPServer.open(0) do |tcp_server| + TCPServer.open(Socket::IPAddress::UNSPECIFIED, 0) do |tcp_server| ssl_server = nil OpenSSL::SSL::Server.open tcp_server, context do |server| server.wrapped.should eq tcp_server diff --git a/spec/std/signal_spec.cr b/spec/std/signal_spec.cr index ee5eff048c73..8b264d6aa49a 100644 --- a/spec/std/signal_spec.cr +++ b/spec/std/signal_spec.cr @@ -3,8 +3,9 @@ require "./spec_helper" require "signal" -# interpreted code never receives signals (#12241) -pending_interpreted describe: "Signal" do +{% skip_file if flag?(:interpreted) && !Crystal::Interpreter.has_method?(:signal) %} + +describe "Signal" do typeof(Signal::ABRT.reset) typeof(Signal::ABRT.ignore) typeof(Signal::ABRT.trap { 1 }) diff --git a/spec/std/socket/address_spec.cr b/spec/std/socket/address_spec.cr index d2e4768db987..08508940bc7d 100644 --- a/spec/std/socket/address_spec.cr +++ b/spec/std/socket/address_spec.cr @@ -51,6 +51,7 @@ describe Socket::IPAddress do addr2.port.should eq(addr1.port) typeof(addr2.address).should eq(String) addr2.address.should eq(addr1.address) + addr2.should eq(Socket::IPAddress.from(addr1_c)) end it "transforms an IPv6 address into a C struct and back" do @@ -64,6 +65,7 @@ describe Socket::IPAddress do addr2.port.should eq(addr1.port) typeof(addr2.address).should eq(String) addr2.address.should eq(addr1.address) + addr2.should eq(Socket::IPAddress.from(addr1_c)) end it "won't resolve domains" do @@ -431,6 +433,7 @@ end addr2.family.should eq(addr1.family) addr2.path.should eq(addr1.path) addr2.to_s.should eq(path) + addr2 = Socket::UNIXAddress.from(addr1.to_unsafe) end it "raises when path is too long" do diff --git a/spec/std/socket/socket_spec.cr b/spec/std/socket/socket_spec.cr index 65f7ed72a453..8bb7349318c6 100644 --- a/spec/std/socket/socket_spec.cr +++ b/spec/std/socket/socket_spec.cr @@ -87,6 +87,8 @@ describe Socket, tags: "network" do expect_raises(IO::TimeoutError) { server.accept } expect_raises(IO::TimeoutError) { server.accept? } + ensure + server.try &.close end it "sends messages" do diff --git a/spec/std/socket/spec_helper.cr b/spec/std/socket/spec_helper.cr index 486e4a142ee7..276e2f4195f2 100644 --- a/spec/std/socket/spec_helper.cr +++ b/spec/std/socket/spec_helper.cr @@ -5,7 +5,7 @@ module SocketSpecHelper class_getter?(supports_ipv6 : Bool) do TCPServer.open("::1", 0) { return true } false - rescue Socket::BindError + rescue Socket::Error false end end @@ -33,7 +33,7 @@ def each_ip_family(&block : Socket::Family, String, String ->) end def unused_local_port - TCPServer.open("::", 0) do |server| + TCPServer.open(Socket::IPAddress::UNSPECIFIED, 0) do |server| server.local_address.port end end diff --git a/spec/std/socket/tcp_server_spec.cr b/spec/std/socket/tcp_server_spec.cr index a7d85b8edeff..451cbbb33d61 100644 --- a/spec/std/socket/tcp_server_spec.cr +++ b/spec/std/socket/tcp_server_spec.cr @@ -120,7 +120,7 @@ describe TCPServer, tags: "network" do it "binds to all interfaces" do port = unused_local_port - TCPServer.open(port) do |server| + TCPServer.open(Socket::IPAddress::UNSPECIFIED, port) do |server| server.local_address.port.should eq port end end diff --git a/spec/std/socket/tcp_socket_spec.cr b/spec/std/socket/tcp_socket_spec.cr index b7a22882ec0f..b44b3a9729f6 100644 --- a/spec/std/socket/tcp_socket_spec.cr +++ b/spec/std/socket/tcp_socket_spec.cr @@ -112,6 +112,8 @@ describe TCPSocket, tags: "network" do end it "fails to connect IPv6 to IPv4 server" do + pending! "IPv6 is unavailable" unless SocketSpecHelper.supports_ipv6? + port = unused_local_port TCPServer.open("0.0.0.0", port) do |server| @@ -133,7 +135,7 @@ describe TCPSocket, tags: "network" do it "sync from server" do port = unused_local_port - TCPServer.open("::", port) do |server| + TCPServer.open(Socket::IPAddress::UNSPECIFIED, port) do |server| TCPSocket.open("localhost", port) do |client| sock = server.accept sock.sync?.should eq(server.sync?) @@ -152,7 +154,7 @@ describe TCPSocket, tags: "network" do it "settings" do port = unused_local_port - TCPServer.open("::", port) do |server| + TCPServer.open(Socket::IPAddress::UNSPECIFIED, port) do |server| TCPSocket.open("localhost", port) do |client| # test protocol specific socket options (client.tcp_nodelay = true).should be_true @@ -202,7 +204,7 @@ describe TCPSocket, tags: "network" do channel = Channel(Exception?).new spawn do - TCPServer.open("::", port) do |server| + TCPServer.open(Socket::IPAddress::UNSPECIFIED, port) do |server| channel.send nil sock = server.accept sock.read_timeout = 3.second diff --git a/spec/std/socket/udp_socket_spec.cr b/spec/std/socket/udp_socket_spec.cr index 6b349072294d..a84a6adebc74 100644 --- a/spec/std/socket/udp_socket_spec.cr +++ b/spec/std/socket/udp_socket_spec.cr @@ -28,6 +28,8 @@ describe UDPSocket, tags: "network" do socket = UDPSocket.new(family) socket.bind(address, 0) socket.local_address.address.should eq address + ensure + socket.try &.close end it "sends and receives messages" do diff --git a/spec/std/string_spec.cr b/spec/std/string_spec.cr index 6d7487ded0e2..0a57ee9034a9 100644 --- a/spec/std/string_spec.cr +++ b/spec/std/string_spec.cr @@ -773,6 +773,24 @@ describe "String" do it { "hello\n\n\n\n".chomp("").should eq("hello\n\n\n\n") } it { "hello\r\n".chomp("\n").should eq("hello") } + + it "pre-computes string size if possible" do + {"!hello!", "\u{1f602}hello\u{1f602}", "\xFEhello\xFF"}.each do |str| + {"", "\n", "\r", "\r\n"}.each do |newline| + x = str + newline + x.size_known?.should be_true + y = x.chomp + y.@length.should eq(7) + end + end + end + + it "does not pre-compute string size if not possible" do + x = String.build &.<< "abc\n" + x.size_known?.should be_false + y = x.chomp + y.size_known?.should be_false + end end describe "lchop" do diff --git a/src/big/lib_gmp.cr b/src/big/lib_gmp.cr index 7368cb0e9fb6..c0e8ef8b2e37 100644 --- a/src/big/lib_gmp.cr +++ b/src/big/lib_gmp.cr @@ -1,3 +1,9 @@ +# Supported library versions: +# +# * libgmp +# * libmpir +# +# See https://crystal-lang.org/reference/man/required_libraries.html#big-numbers {% if flag?(:win32) && !flag?(:gnu) %} @[Link("mpir")] {% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %} diff --git a/src/compiler/crystal/codegen/target.cr b/src/compiler/crystal/codegen/target.cr index 223d64fe859b..cf11ed96fef4 100644 --- a/src/compiler/crystal/codegen/target.cr +++ b/src/compiler/crystal/codegen/target.cr @@ -192,6 +192,10 @@ class Crystal::Codegen::Target @architecture == "avr" end + def embedded? + environment_parts.any? { |part| part == "eabi" || part == "eabihf" } + end + def to_target_machine(cpu = "", features = "", optimization_mode = Compiler::OptimizationMode::O0, code_model = LLVM::CodeModel::Default) : LLVM::TargetMachine case @architecture @@ -228,8 +232,14 @@ class Crystal::Codegen::Target in .o0? then LLVM::CodeGenOptLevel::None end + if embedded? + reloc = LLVM::RelocMode::Static + else + reloc = LLVM::RelocMode::PIC + end + target = LLVM::Target.from_triple(self.to_s) - machine = target.create_target_machine(self.to_s, cpu: cpu, features: features, opt_level: opt_level, code_model: code_model).not_nil! + machine = target.create_target_machine(self.to_s, cpu: cpu, features: features, opt_level: opt_level, reloc: reloc, code_model: code_model).not_nil! # FIXME: We need to disable global isel until https://reviews.llvm.org/D80898 is released, # or we fixed generating values for 0 sized types. # When removing this, also remove it from the ABI specs and jit compiler. diff --git a/src/compiler/crystal/ffi/lib_ffi.cr b/src/compiler/crystal/ffi/lib_ffi.cr index 2d08cf4e18dd..22929279c09e 100644 --- a/src/compiler/crystal/ffi/lib_ffi.cr +++ b/src/compiler/crystal/ffi/lib_ffi.cr @@ -1,3 +1,8 @@ +# Supported library versions: +# +# * libffi +# +# See https://crystal-lang.org/reference/man/required_libraries.html#compiler-dependencies module Crystal @[Link("ffi")] {% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %} diff --git a/src/compiler/crystal/interpreter/instructions.cr b/src/compiler/crystal/interpreter/instructions.cr index 23428df03b90..f8f986f1b44a 100644 --- a/src/compiler/crystal/interpreter/instructions.cr +++ b/src/compiler/crystal/interpreter/instructions.cr @@ -1675,6 +1675,15 @@ require "./repl" code: fiber_resumable(context), }, + interpreter_signal_descriptor: { + pop_values: [fd : Int32], + code: signal_descriptor(fd), + }, + interpreter_signal: { + pop_values: [signum : Int32, handler : Int32], + code: signal(signum, handler), + }, + {% if flag?(:bits64) %} interpreter_intrinsics_memcpy: { pop_values: [dest : Pointer(Void), src : Pointer(Void), len : UInt64, is_volatile : Bool], diff --git a/src/compiler/crystal/interpreter/interpreter.cr b/src/compiler/crystal/interpreter/interpreter.cr index e26a6751c176..c084ff43e910 100644 --- a/src/compiler/crystal/interpreter/interpreter.cr +++ b/src/compiler/crystal/interpreter/interpreter.cr @@ -1174,6 +1174,38 @@ class Crystal::Repl::Interpreter fiber.@context.resumable end + private def signal_descriptor(fd : Int32) : Nil + {% if flag?(:unix) %} + # replace the interpreter's signal writer so that the interpreted code + # will receive signals from now on + writer = IO::FileDescriptor.new(fd) + writer.sync = true + Crystal::System::Signal.writer = writer + {% else %} + raise "BUG: interpreter doesn't support signals on this target" + {% end %} + end + + private def signal(signum : Int32, handler : Int32) : Nil + {% if flag?(:unix) %} + signal = ::Signal.new(signum) + case handler + when 0 + signal.reset + when 1 + signal.ignore + else + # register the signal for the OS so the process will receive them; + # registers a fake handler since the interpreter won't handle the signal: + # the interpreted code will receive it and will execute the interpreted + # handler + signal.trap { } + end + {% else %} + raise "BUG: interpreter doesn't support signals on this target" + {% end %} + end + private def pry(ip, instructions, stack_bottom, stack) offset = (ip - instructions.instructions.to_unsafe).to_i32 node = instructions.nodes[offset]? diff --git a/src/compiler/crystal/interpreter/primitives.cr b/src/compiler/crystal/interpreter/primitives.cr index ca436947370e..619f678ad6bd 100644 --- a/src/compiler/crystal/interpreter/primitives.cr +++ b/src/compiler/crystal/interpreter/primitives.cr @@ -431,6 +431,12 @@ class Crystal::Repl::Compiler when "interpreter_fiber_resumable" accept_call_args(node) interpreter_fiber_resumable(node: node) + when "interpreter_signal_descriptor" + accept_call_args(node) + interpreter_signal_descriptor(node: node) + when "interpreter_signal" + accept_call_args(node) + interpreter_signal(node: node) when "interpreter_intrinsics_memcpy" accept_call_args(node) interpreter_intrinsics_memcpy(node: node) diff --git a/src/compiler/crystal/loader/mingw.cr b/src/compiler/crystal/loader/mingw.cr index 677f564cec16..2c557a893640 100644 --- a/src/compiler/crystal/loader/mingw.cr +++ b/src/compiler/crystal/loader/mingw.cr @@ -7,6 +7,8 @@ require "crystal/system/win32/library_archive" # The core implementation is derived from the MSVC loader. Main deviations are: # # - `.parse` follows GNU `ld`'s style, rather than MSVC `link`'s; +# - `.parse` automatically inserts a C runtime library if `-mcrtdll` isn't +# supplied; # - `#library_filename` follows the usual naming of the MinGW linker: `.dll.a` # for DLL import libraries, `.a` for other libraries; # - `.default_search_paths` relies solely on `.cc_each_library_path`. @@ -28,6 +30,11 @@ class Crystal::Loader file_paths = [] of String extra_search_paths = [] of String + # note that `msvcrt` is a default runtime chosen at MinGW-w64 build time, + # `ucrt` is always UCRT (even in a MINGW64 environment), and + # `msvcrt-os` is always MSVCRT (even in a UCRT64 environment) + crt_dll = "msvcrt" + OptionParser.parse(args.dup) do |parser| parser.on("-L DIRECTORY", "--library-path DIRECTORY", "Add DIRECTORY to library search path") do |directory| extra_search_paths << directory @@ -39,17 +46,21 @@ class Crystal::Loader raise LoadError.new "static libraries are not supported by Crystal's runtime loader" end parser.unknown_args do |args, after_dash| - file_paths.concat args + file_paths.concat args.reject(&.starts_with?("-mcrtdll=")) end parser.invalid_option do |arg| - unless arg.starts_with?("-Wl,") + if crt_dll_arg = arg.lchop?("-mcrtdll=") + # the GCC spec is `%{!mcrtdll=*:-lmsvcrt} %{mcrtdll=*:-l%*}` + crt_dll = crt_dll_arg + elsif !arg.starts_with?("-Wl,") raise LoadError.new "Not a recognized linker flag: #{arg}" end end end search_paths = extra_search_paths + search_paths + libnames << crt_dll begin loader = new(search_paths) diff --git a/src/concurrent.cr b/src/concurrent.cr index 0f8805857720..07ae945a84f6 100644 --- a/src/concurrent.cr +++ b/src/concurrent.cr @@ -33,20 +33,23 @@ end # Spawns a new fiber. # -# The newly created fiber doesn't run as soon as spawned. +# NOTE: The newly created fiber doesn't run as soon as spawned. # # Example: # ``` # # Write "1" every 1 second and "2" every 2 seconds for 6 seconds. # -# ch = Channel(Nil).new +# require "wait_group" +# +# wg = WaitGroup.new 2 # # spawn do # 6.times do # sleep 1.second # puts 1 # end -# ch.send(nil) +# ensure +# wg.done # end # # spawn do @@ -54,10 +57,11 @@ end # sleep 2.seconds # puts 2 # end -# ch.send(nil) +# ensure +# wg.done # end # -# 2.times { ch.receive } +# wg.wait # ``` def spawn(*, name : String? = nil, same_thread = false, &block) fiber = Fiber.new(name, &block) diff --git a/src/crystal/interpreter.cr b/src/crystal/interpreter.cr index d3b3589d50cb..bad67420f5f3 100644 --- a/src/crystal/interpreter.cr +++ b/src/crystal/interpreter.cr @@ -24,5 +24,15 @@ module Crystal @[Primitive(:interpreter_fiber_resumable)] def self.fiber_resumable(context) : LibC::Long end + + {% if compare_versions(Crystal::VERSION, "1.15.0-dev") >= 0 %} + @[Primitive(:interpreter_signal_descriptor)] + def self.signal_descriptor(fd : Int32) : Nil + end + + @[Primitive(:interpreter_signal)] + def self.signal(signum : Int32, handler : Int32) : Nil + end + {% end %} end end diff --git a/src/crystal/lib_iconv.cr b/src/crystal/lib_iconv.cr index 07100ff9c1dc..dafcb7a75d53 100644 --- a/src/crystal/lib_iconv.cr +++ b/src/crystal/lib_iconv.cr @@ -4,6 +4,11 @@ require "c/stddef" {% raise "The `without_iconv` flag is preventing you to use the LibIconv module" %} {% end %} +# Supported library versions: +# +# * libiconv-gnu +# +# See https://crystal-lang.org/reference/man/required_libraries.html#internationalization-conversion @[Link("iconv")] {% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %} @[Link(dll: "iconv-2.dll")] diff --git a/src/crystal/system/event_loop.cr b/src/crystal/system/event_loop.cr index fe973ec8c99e..33ff4f9dac85 100644 --- a/src/crystal/system/event_loop.cr +++ b/src/crystal/system/event_loop.cr @@ -4,7 +4,16 @@ abstract class Crystal::EventLoop {% if flag?(:wasi) %} Crystal::Wasi::EventLoop.new {% elsif flag?(:unix) %} - Crystal::LibEvent::EventLoop.new + # TODO: enable more targets by default (need manual tests or fixes) + {% if flag?("evloop=libevent") %} + Crystal::LibEvent::EventLoop.new + {% elsif flag?("evloop=epoll") || flag?(:android) || flag?(:linux) %} + Crystal::Epoll::EventLoop.new + {% elsif flag?("evloop=kqueue") || flag?(:darwin) || flag?(:freebsd) %} + Crystal::Kqueue::EventLoop.new + {% else %} + Crystal::LibEvent::EventLoop.new + {% end %} {% elsif flag?(:win32) %} Crystal::IOCP::EventLoop.new {% else %} @@ -78,7 +87,15 @@ end {% if flag?(:wasi) %} require "./wasi/event_loop" {% elsif flag?(:unix) %} - require "./unix/event_loop_libevent" + {% if flag?("evloop=libevent") %} + require "./unix/event_loop_libevent" + {% elsif flag?("evloop=epoll") || flag?(:android) || flag?(:linux) %} + require "./unix/epoll/event_loop" + {% elsif flag?("evloop=kqueue") || flag?(:darwin) || flag?(:freebsd) %} + require "./unix/kqueue/event_loop" + {% else %} + require "./unix/event_loop_libevent" + {% end %} {% elsif flag?(:win32) %} require "./win32/event_loop_iocp" {% else %} diff --git a/src/crystal/system/event_loop/socket.cr b/src/crystal/system/event_loop/socket.cr index 6309aed391e0..8fa86e50affc 100644 --- a/src/crystal/system/event_loop/socket.cr +++ b/src/crystal/system/event_loop/socket.cr @@ -12,7 +12,7 @@ abstract class Crystal::EventLoop # Returns the number of bytes read (up to `slice.size`). # Returns 0 when the socket is closed and no data available. # - # Use `#send_to` for sending a message to a specific target address. + # Use `#receive_from` for capturing the source address of a message. abstract def read(socket : ::Socket, slice : Bytes) : Int32 # Writes at least one byte from *slice* to the socket. @@ -22,7 +22,7 @@ abstract class Crystal::EventLoop # # Returns the number of bytes written (up to `slice.size`). # - # Use `#receive_from` for capturing the source address of a message. + # Use `#send_to` for sending a message to a specific target address. abstract def write(socket : ::Socket, slice : Bytes) : Int32 # Accepts an incoming TCP connection on the socket. diff --git a/src/crystal/system/unix/epoll.cr b/src/crystal/system/unix/epoll.cr new file mode 100644 index 000000000000..28a157ae3360 --- /dev/null +++ b/src/crystal/system/unix/epoll.cr @@ -0,0 +1,66 @@ +require "c/sys/epoll" + +struct Crystal::System::Epoll + def initialize + @epfd = LibC.epoll_create1(LibC::EPOLL_CLOEXEC) + raise RuntimeError.from_errno("epoll_create1") if @epfd == -1 + end + + def fd : Int32 + @epfd + end + + def add(fd : Int32, epoll_event : LibC::EpollEvent*) : Nil + if LibC.epoll_ctl(@epfd, LibC::EPOLL_CTL_ADD, fd, epoll_event) == -1 + raise RuntimeError.from_errno("epoll_ctl(EPOLL_CTL_ADD)") unless Errno.value == Errno::EPERM + end + end + + def add(fd : Int32, events : UInt32, u64 : UInt64) : Nil + epoll_event = uninitialized LibC::EpollEvent + epoll_event.events = events + epoll_event.data.u64 = u64 + add(fd, pointerof(epoll_event)) + end + + def modify(fd : Int32, epoll_event : LibC::EpollEvent*) : Nil + if LibC.epoll_ctl(@epfd, LibC::EPOLL_CTL_MOD, fd, epoll_event) == -1 + raise RuntimeError.from_errno("epoll_ctl(EPOLL_CTL_MOD)") + end + end + + def delete(fd : Int32) : Nil + delete(fd) do + raise RuntimeError.from_errno("epoll_ctl(EPOLL_CTL_DEL)") + end + end + + def delete(fd : Int32, &) : Nil + if LibC.epoll_ctl(@epfd, LibC::EPOLL_CTL_DEL, fd, nil) == -1 + yield + end + end + + # `timeout` is in milliseconds; -1 will wait indefinitely; 0 will never wait. + def wait(events : Slice(LibC::EpollEvent), timeout : Int32) : Slice(LibC::EpollEvent) + count = 0 + + loop do + count = LibC.epoll_wait(@epfd, events.to_unsafe, events.size, timeout) + break unless count == -1 + + if Errno.value == Errno::EINTR + # retry when waiting indefinitely, return otherwise + break unless timeout == -1 + else + raise RuntimeError.from_errno("epoll_wait") + end + end + + events[0, count.clamp(0..)] + end + + def close : Nil + LibC.close(@epfd) + end +end diff --git a/src/crystal/system/unix/epoll/event_loop.cr b/src/crystal/system/unix/epoll/event_loop.cr new file mode 100644 index 000000000000..dc2f2052dfa2 --- /dev/null +++ b/src/crystal/system/unix/epoll/event_loop.cr @@ -0,0 +1,142 @@ +require "../evented/event_loop" +require "../epoll" +require "../eventfd" +require "../timerfd" + +class Crystal::Epoll::EventLoop < Crystal::Evented::EventLoop + def initialize + # the epoll instance + @epoll = System::Epoll.new + + # notification to interrupt a run + @interrupted = Atomic::Flag.new + @eventfd = System::EventFD.new + @epoll.add(@eventfd.fd, LibC::EPOLLIN, u64: @eventfd.fd.to_u64!) + + # we use timerfd to go below the millisecond precision of epoll_wait; it + # also allows to avoid locking timers before every epoll_wait call + @timerfd = System::TimerFD.new + @epoll.add(@timerfd.fd, LibC::EPOLLIN, u64: @timerfd.fd.to_u64!) + end + + def after_fork_before_exec : Nil + super + + # O_CLOEXEC would close these automatically, but we don't want to mess with + # the parent process fds (it would mess the parent evloop) + @epoll.close + @eventfd.close + @timerfd.close + end + + {% unless flag?(:preview_mt) %} + def after_fork : Nil + super + + # close inherited fds + @epoll.close + @eventfd.close + @timerfd.close + + # create new fds + @epoll = System::Epoll.new + + @interrupted.clear + @eventfd = System::EventFD.new + @epoll.add(@eventfd.fd, LibC::EPOLLIN, u64: @eventfd.fd.to_u64!) + + @timerfd = System::TimerFD.new + @epoll.add(@timerfd.fd, LibC::EPOLLIN, u64: @timerfd.fd.to_u64!) + system_set_timer(@timers.next_ready?) + + # re-add all registered fds + Evented.arena.each { |fd, index| system_add(fd, index) } + end + {% end %} + + private def system_run(blocking : Bool) : Nil + Crystal.trace :evloop, "run", blocking: blocking ? 1 : 0 + + # wait for events (indefinitely when blocking) + buffer = uninitialized LibC::EpollEvent[128] + epoll_events = @epoll.wait(buffer.to_slice, timeout: blocking ? -1 : 0) + + timer_triggered = false + + # process events + epoll_events.size.times do |i| + epoll_event = epoll_events.to_unsafe + i + + case epoll_event.value.data.u64 + when @eventfd.fd + # TODO: panic if epoll_event.value.events != LibC::EPOLLIN (could be EPOLLERR or EPLLHUP) + Crystal.trace :evloop, "interrupted" + @eventfd.read + # OPTIMIZE: only reset interrupted before a blocking wait + @interrupted.clear + when @timerfd.fd + # TODO: panic if epoll_event.value.events != LibC::EPOLLIN (could be EPOLLERR or EPLLHUP) + Crystal.trace :evloop, "timer" + timer_triggered = true + else + process_io(epoll_event) + end + end + + process_timers(timer_triggered) + end + + private def process_io(epoll_event : LibC::EpollEvent*) : Nil + index = Evented::Arena::Index.new(epoll_event.value.data.u64) + events = epoll_event.value.events + + Crystal.trace :evloop, "event", fd: index.index, index: index.to_i64, events: events + + Evented.arena.get?(index) do |pd| + if (events & (LibC::EPOLLERR | LibC::EPOLLHUP)) != 0 + pd.value.@readers.ready_all { |event| unsafe_resume_io(event) } + pd.value.@writers.ready_all { |event| unsafe_resume_io(event) } + return + end + + if (events & LibC::EPOLLRDHUP) == LibC::EPOLLRDHUP + pd.value.@readers.ready_all { |event| unsafe_resume_io(event) } + elsif (events & LibC::EPOLLIN) == LibC::EPOLLIN + pd.value.@readers.ready_one { |event| unsafe_resume_io(event) } + end + + if (events & LibC::EPOLLOUT) == LibC::EPOLLOUT + pd.value.@writers.ready_one { |event| unsafe_resume_io(event) } + end + end + end + + def interrupt : Nil + # the atomic makes sure we only write once + @eventfd.write(1) if @interrupted.test_and_set + end + + protected def system_add(fd : Int32, index : Evented::Arena::Index) : Nil + Crystal.trace :evloop, "epoll_ctl", op: "add", fd: fd, index: index.to_i64 + events = LibC::EPOLLIN | LibC::EPOLLOUT | LibC::EPOLLRDHUP | LibC::EPOLLET + @epoll.add(fd, events, u64: index.to_u64) + end + + protected def system_del(fd : Int32, closing = true) : Nil + Crystal.trace :evloop, "epoll_ctl", op: "del", fd: fd + @epoll.delete(fd) + end + + protected def system_del(fd : Int32, closing = true, &) : Nil + Crystal.trace :evloop, "epoll_ctl", op: "del", fd: fd + @epoll.delete(fd) { yield } + end + + private def system_set_timer(time : Time::Span?) : Nil + if time + @timerfd.set(time) + else + @timerfd.cancel + end + end +end diff --git a/src/crystal/system/unix/evented/arena.cr b/src/crystal/system/unix/evented/arena.cr new file mode 100644 index 000000000000..818b80b83c41 --- /dev/null +++ b/src/crystal/system/unix/evented/arena.cr @@ -0,0 +1,257 @@ +# Generational Arena. +# +# Allocates a `Slice` of `T` through `mmap`. `T` is supposed to be a struct, so +# it can be embedded right into the memory region. +# +# The arena allocates objects `T` at a predefined index. The object iself is +# uninitialized (outside of having its memory initialized to zero). The object +# can be allocated and later retrieved using the generation index +# (Arena::Index) that contains both the actual index (Int32) and the generation +# number (UInt32). Deallocating the object increases the generation number, +# which allows the object to be reallocated later on. Trying to retrieve the +# allocation using the generation index will fail if the generation number +# changed (it's a new allocation). +# +# This arena isn't generic as it won't keep a list of free indexes. It assumes +# that something else will maintain the uniqueness of indexes and reuse indexes +# as much as possible instead of growing. +# +# For example this arena is used to hold `Crystal::Evented::PollDescriptor` +# allocations for all the fd in a program, where the fd is used as the index. +# They're unique to the process and the OS always reuses the lowest fd numbers +# before growing. +# +# Thread safety: the memory region is pre-allocated (up to capacity) using mmap +# (virtual allocation) and pointers are never invalidated. Individual +# allocation, deallocation and regular accesses are protected by a fine grained +# lock over each object: parallel accesses to the memory region are prohibited, +# and pointers are expected to not outlive the block that yielded them (don't +# capture them). +# +# Guarantees: `mmap` initializes the memory to zero, which means `T` objects are +# initialized to zero by default, then `#free` will also clear the memory, so +# the next allocation shall be initialized to zero, too. +# +# TODO: instead of the mmap that must preallocate a fixed chunk of virtual +# memory, we could allocate individual blocks of memory, then access the actual +# block at `index % size`. Pointers would still be valid (as long as the block +# isn't collected). We wouldn't have to worry about maximum capacity, we could +# still allocate blocks discontinuously & collect unused blocks during GC +# collections. +class Crystal::Evented::Arena(T) + INVALID_INDEX = Index.new(-1, 0) + + struct Index + def initialize(index : Int32, generation : UInt32) + @data = (index.to_i64! << 32) | generation.to_u64! + end + + def initialize(@data : Int64) + end + + def initialize(data : UInt64) + @data = data.to_i64! + end + + # Returns the generation number. + def generation : UInt32 + @data.to_u32! + end + + # Returns the actual index. + def index : Int32 + (@data >> 32).to_i32! + end + + def to_i64 : Int64 + @data + end + + def to_u64 : UInt64 + @data.to_u64! + end + + def valid? : Bool + @data >= 0 + end + end + + struct Entry(T) + @lock = SpinLock.new # protects parallel allocate/free calls + property? allocated = false + property generation = 0_u32 + @object = uninitialized T + + def pointer : Pointer(T) + pointerof(@object) + end + + def free : Nil + @generation &+= 1_u32 + @allocated = false + pointer.clear(1) + end + end + + @buffer : Slice(Entry(T)) + + {% unless flag?(:preview_mt) %} + # Remember the maximum allocated fd ever; + # + # This is specific to `EventLoop#after_fork` that needs to iterate the arena + # for registered fds in epoll/kqueue to re-add them to the new epoll/kqueue + # instances. Without this upper limit we'd iterate the whole arena which + # would lead the kernel to try and allocate the whole mmap in physical + # memory (instead of virtual memory) which would at best be a waste, and a + # worst fill the memory (e.g. unlimited open files). + @maximum = 0 + {% end %} + + def initialize(capacity : Int32) + pointer = self.class.mmap(LibC::SizeT.new(sizeof(Entry(T))) * capacity) + @buffer = Slice.new(pointer.as(Pointer(Entry(T))), capacity) + end + + protected def self.mmap(bytesize) + flags = LibC::MAP_PRIVATE | LibC::MAP_ANON + prot = LibC::PROT_READ | LibC::PROT_WRITE + + pointer = LibC.mmap(nil, bytesize, prot, flags, -1, 0) + System.panic("mmap", Errno.value) if pointer == LibC::MAP_FAILED + + {% if flag?(:linux) %} + LibC.madvise(pointer, bytesize, LibC::MADV_NOHUGEPAGE) + {% end %} + + pointer + end + + def finalize + LibC.munmap(@buffer.to_unsafe, @buffer.bytesize) + end + + # Allocates the object at *index* unless already allocated, then yields a + # pointer to the object at *index* and the current generation index to later + # retrieve and free the allocated object. Eventually returns the generation + # index. + # + # Does nothing if the object has already been allocated and returns `nil`. + # + # There are no generational checks. + # Raises if *index* is out of bounds. + def allocate_at?(index : Int32, & : (Pointer(T), Index) ->) : Index? + entry = at(index) + + entry.value.@lock.sync do + return if entry.value.allocated? + + {% unless flag?(:preview_mt) %} + @maximum = index if index > @maximum + {% end %} + entry.value.allocated = true + + gen_index = Index.new(index, entry.value.generation) + yield entry.value.pointer, gen_index + + gen_index + end + end + + # Same as `#allocate_at?` but raises when already allocated. + def allocate_at(index : Int32, & : (Pointer(T), Index) ->) : Index? + allocate_at?(index) { |ptr, idx| yield ptr, idx } || + raise RuntimeError.new("#{self.class.name}: already allocated index=#{index}") + end + + # Yields a pointer to the object previously allocated at *index*. + # + # Raises if the object isn't allocated. + # Raises if the generation has changed (i.e. the object has been freed then reallocated). + # Raises if *index* is negative. + def get(index : Index, &) : Nil + at(index) do |entry| + yield entry.value.pointer + end + end + + # Yields a pointer to the object previously allocated at *index* and returns + # true. + # Does nothing if the object isn't allocated or the generation has changed, + # and returns false. + # + # Raises if *index* is negative. + def get?(index : Index, &) : Bool + at?(index) do |entry| + yield entry.value.pointer + return true + end + false + end + + # Yields the object previously allocated at *index* then releases it. + # Does nothing if the object isn't allocated or the generation has changed. + # + # Raises if *index* is negative. + def free(index : Index, &) : Nil + at?(index) do |entry| + begin + yield entry.value.pointer + ensure + entry.value.free + end + end + end + + private def at(index : Index, &) : Nil + entry = at(index.index) + entry.value.@lock.lock + + unless entry.value.allocated? && entry.value.generation == index.generation + entry.value.@lock.unlock + raise RuntimeError.new("#{self.class.name}: invalid reference index=#{index.index}:#{index.generation} current=#{index.index}:#{entry.value.generation}") + end + + begin + yield entry + ensure + entry.value.@lock.unlock + end + end + + private def at?(index : Index, &) : Nil + return unless entry = at?(index.index) + + entry.value.@lock.sync do + return unless entry.value.allocated? + return unless entry.value.generation == index.generation + + yield entry + end + end + + private def at(index : Int32) : Pointer(Entry(T)) + (@buffer + index).to_unsafe + end + + private def at?(index : Int32) : Pointer(Entry(T))? + if 0 <= index < @buffer.size + @buffer.to_unsafe + index + end + end + + {% unless flag?(:preview_mt) %} + # Iterates all allocated objects, yields the actual index as well as the + # generation index. + def each(&) : Nil + pointer = @buffer.to_unsafe + + 0.upto(@maximum) do |index| + entry = pointer + index + + if entry.value.allocated? + yield index, Index.new(index, entry.value.generation) + end + end + end + {% end %} +end diff --git a/src/crystal/system/unix/evented/event.cr b/src/crystal/system/unix/evented/event.cr new file mode 100644 index 000000000000..b33130df53c2 --- /dev/null +++ b/src/crystal/system/unix/evented/event.cr @@ -0,0 +1,58 @@ +require "crystal/pointer_linked_list" + +# Information about the event that a `Fiber` is waiting on. +# +# The event can be waiting for `IO` with or without a timeout, or be a timed +# event such as sleep or a select timeout (without IO). +# +# The events can be found in different queues, for example `Timers` and/or +# `Waiters` depending on their type. +struct Crystal::Evented::Event + enum Type + IoRead + IoWrite + Sleep + SelectTimeout + end + + getter type : Type + + # The `Fiber` that is waiting on the event and that the `EventLoop` shall + # resume. + getter fiber : Fiber + + # Arena index to access the associated `PollDescriptor` when processing an IO + # event. Nil for timed events (sleep, select timeout). + getter! index : Arena::Index? + + # The absolute time, against the monotonic clock, at which a timed event shall + # trigger. Nil for IO events without a timeout. + getter! wake_at : Time::Span + + # True if an IO event has timed out (i.e. we're past `#wake_at`). + getter? timed_out : Bool = false + + # The event can be added to `Waiters` lists. + include PointerLinkedList::Node + + def initialize(@type : Type, @fiber, @index = nil, timeout : Time::Span? = nil) + if timeout + seconds, nanoseconds = System::Time.monotonic + now = Time::Span.new(seconds: seconds, nanoseconds: nanoseconds) + @wake_at = now + timeout + end + end + + # Mark the IO event as timed out. + def timed_out! : Bool + @timed_out = true + end + + # Manually set the absolute time (against the monotonic clock). This is meant + # for `FiberEvent` to set and cancel its inner sleep or select timeout; these + # objects are allocated once per `Fiber`. + # + # NOTE: musn't be changed after registering the event into `Timers`! + def wake_at=(@wake_at) + end +end diff --git a/src/crystal/system/unix/evented/event_loop.cr b/src/crystal/system/unix/evented/event_loop.cr new file mode 100644 index 000000000000..65b9e746b9b2 --- /dev/null +++ b/src/crystal/system/unix/evented/event_loop.cr @@ -0,0 +1,521 @@ +require "./*" +require "./arena" + +module Crystal::System::FileDescriptor + # user data (generation index for the arena) + property __evloop_data : Evented::Arena::Index = Evented::Arena::INVALID_INDEX +end + +module Crystal::System::Socket + # user data (generation index for the arena) + property __evloop_data : Evented::Arena::Index = Evented::Arena::INVALID_INDEX +end + +module Crystal::Evented + # The generational arena: + # + # 1. decorrelates the fd from the IO since the evloop only really cares about + # the fd state and to resume pending fibers (it could monitor a fd without + # an IO object); + # + # 2. permits to avoid pushing raw pointers to IO objects into kernel data + # structures that are unknown to the GC, and to safely check whether the + # allocation is still valid before trying to dereference the pointer. Since + # `PollDescriptor` also doesn't have pointers to the actual IO object, it + # won't prevent the GC from collecting lost IO objects (and spares us from + # using weak references). + # + # 3. to a lesser extent, it also allows to keep the `PollDescriptor` allocated + # together in the same region, and polluting the IO object itself with + # specific evloop data (except for the generation index). + # + # The implementation takes advantage of the fd being unique per process and + # that the operating system will always reuse the lowest fd (POSIX compliance) + # and will only grow when the process needs that many file descriptors, so the + # allocated memory region won't grow larger than necessary. This assumption + # allows the arena to skip maintaining a list of free indexes. Some systems + # may deviate from the POSIX default, but all systems seem to follow it, as it + # allows optimizations to the OS (it can reuse already allocated resources), + # and either the man page explicitly says so (Linux), or they don't (BSD) and + # they must follow the POSIX definition. + protected class_getter arena = Arena(PollDescriptor).new(max_fds) + + private def self.max_fds : Int32 + if LibC.getrlimit(LibC::RLIMIT_NOFILE, out rlimit) == -1 + raise RuntimeError.from_errno("getrlimit(RLIMIT_NOFILE)") + end + rlimit.rlim_cur.clamp(..Int32::MAX).to_i32! + end +end + +# Polling EventLoop. +# +# This is the abstract interface that implements `Crystal::EventLoop` for +# polling based UNIX targets, such as epoll (linux), kqueue (bsd), or poll +# (posix) syscalls. This class only implements the generic parts for the +# external world to interact with the loop. A specific implementation is +# required to handle the actual syscalls. See `Crystal::Epoll::EventLoop` and +# `Crystal::Kqueue::EventLoop`. +# +# The event loop registers the fd into the kernel data structures when an IO +# operation would block, then keeps it there until the fd is closed. +# +# NOTE: the fds must have `O_NONBLOCK` set. +# +# It is possible to have multiple event loop instances, but an fd can only be in +# one instance at a time. When trying to block from another loop, the fd will be +# removed from its associated loop and added to the current one (this is +# automatic). Trying to move a fd to another loop with pending waiters is +# unsupported and will raise an exception. See `PollDescriptor#remove`. +# +# A timed event such as sleep or select timeout follows the following logic: +# +# 1. create an `Event` (actually reuses it, see `FiberChannel`); +# 2. register the event in `@timers`; +# 3. supend the current fiber. +# +# The timer will eventually trigger and resume the fiber. +# When an IO operation on fd would block, the loop follows the following logic: +# +# 1. register the fd (once); +# 2. create an `Event`; +# 3. suspend the current fiber; +# +# When the IO operation is ready, the fiber will eventually be resumed (one +# fiber at a time). If it's an IO operation, the operation is tried again which +# may block again, until the operation succeeds or an error occured (e.g. +# closed, broken pipe). +# +# If the IO operation has a timeout, the event is also registered into `@timers` +# before suspending the fiber, then after resume it will raise +# `IO::TimeoutError` if the event timed out, and continue otherwise. +abstract class Crystal::Evented::EventLoop < Crystal::EventLoop + @lock = SpinLock.new # protects parallel accesses to @timers + @timers = Timers.new + + # reset the mutexes since another thread may have acquired the lock of one + # event loop, which would prevent closing file descriptors for example. + def after_fork_before_exec : Nil + @lock = SpinLock.new + end + + {% unless flag?(:preview_mt) %} + # no parallelism issues, but let's clean-up anyway + def after_fork : Nil + @lock = SpinLock.new + end + {% end %} + + # NOTE: thread unsafe + def run(blocking : Bool) : Bool + system_run(blocking) + true + end + + # fiber interface, see Crystal::EventLoop + + def create_resume_event(fiber : Fiber) : FiberEvent + FiberEvent.new(self, fiber, :sleep) + end + + def create_timeout_event(fiber : Fiber) : FiberEvent + FiberEvent.new(self, fiber, :select_timeout) + end + + # file descriptor interface, see Crystal::EventLoop::FileDescriptor + + def read(file_descriptor : System::FileDescriptor, slice : Bytes) : Int32 + size = evented_read(file_descriptor, slice, file_descriptor.@read_timeout) + + if size == -1 + if Errno.value == Errno::EBADF + raise IO::Error.new("File not open for reading", target: file_descriptor) + else + raise IO::Error.from_errno("read", target: file_descriptor) + end + else + size.to_i32 + end + end + + def write(file_descriptor : System::FileDescriptor, slice : Bytes) : Int32 + size = evented_write(file_descriptor, slice, file_descriptor.@write_timeout) + + if size == -1 + if Errno.value == Errno::EBADF + raise IO::Error.new("File not open for writing", target: file_descriptor) + else + raise IO::Error.from_errno("write", target: file_descriptor) + end + else + size.to_i32 + end + end + + def close(file_descriptor : System::FileDescriptor) : Nil + evented_close(file_descriptor) + end + + def remove(file_descriptor : System::FileDescriptor) : Nil + internal_remove(file_descriptor) + end + + # socket interface, see Crystal::EventLoop::Socket + + def read(socket : ::Socket, slice : Bytes) : Int32 + size = evented_read(socket, slice, socket.@read_timeout) + raise IO::Error.from_errno("read", target: socket) if size == -1 + size + end + + def write(socket : ::Socket, slice : Bytes) : Int32 + size = evented_write(socket, slice, socket.@write_timeout) + raise IO::Error.from_errno("write", target: socket) if size == -1 + size + end + + def accept(socket : ::Socket) : ::Socket::Handle? + loop do + client_fd = + {% if LibC.has_method?(:accept4) %} + LibC.accept4(socket.fd, nil, nil, LibC::SOCK_CLOEXEC) + {% else %} + # we may fail to set FD_CLOEXEC between `accept` and `fcntl` but we + # can't call `Crystal::System::Socket.lock_read` because the socket + # might be in blocking mode and accept would block until the socket + # receives a connection. + # + # we could lock when `socket.blocking?` is false, but another thread + # could change the socket back to blocking mode between the condition + # check and the `accept` call. + LibC.accept(socket.fd, nil, nil).tap do |fd| + System::Socket.fcntl(fd, LibC::F_SETFD, LibC::FD_CLOEXEC) unless fd == -1 + end + {% end %} + + return client_fd unless client_fd == -1 + return if socket.closed? + + if Errno.value == Errno::EAGAIN + wait_readable(socket, socket.@read_timeout) do + raise IO::TimeoutError.new("Accept timed out") + end + return if socket.closed? + else + raise ::Socket::Error.from_errno("accept") + end + end + end + + def connect(socket : ::Socket, address : ::Socket::Addrinfo | ::Socket::Address, timeout : Time::Span?) : IO::Error? + loop do + ret = LibC.connect(socket.fd, address, address.size) + return unless ret == -1 + + case Errno.value + when Errno::EISCONN + return + when Errno::EINPROGRESS, Errno::EALREADY + wait_writable(socket, timeout) do + return IO::TimeoutError.new("Connect timed out") + end + else + return ::Socket::ConnectError.from_errno("connect") + end + end + end + + def send_to(socket : ::Socket, slice : Bytes, address : ::Socket::Address) : Int32 + bytes_sent = LibC.sendto(socket.fd, slice.to_unsafe.as(Void*), slice.size, 0, address, address.size) + raise ::Socket::Error.from_errno("Error sending datagram to #{address}") if bytes_sent == -1 + bytes_sent.to_i32 + end + + def receive_from(socket : ::Socket, slice : Bytes) : {Int32, ::Socket::Address} + sockaddr = Pointer(LibC::SockaddrStorage).malloc.as(LibC::Sockaddr*) + + # initialize sockaddr with the initialized family of the socket + copy = sockaddr.value + copy.sa_family = socket.family + sockaddr.value = copy + addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrStorage)) + + loop do + size = LibC.recvfrom(socket.fd, slice, slice.size, 0, sockaddr, pointerof(addrlen)) + if size == -1 + if Errno.value == Errno::EAGAIN + wait_readable(socket, socket.@read_timeout) + check_open(socket) + else + raise IO::Error.from_errno("recvfrom", target: socket) + end + else + return {size.to_i32, ::Socket::Address.from(sockaddr, addrlen)} + end + end + end + + def close(socket : ::Socket) : Nil + evented_close(socket) + end + + def remove(socket : ::Socket) : Nil + internal_remove(socket) + end + + # internals: IO + + private def evented_read(io, slice : Bytes, timeout : Time::Span?) : Int32 + loop do + ret = LibC.read(io.fd, slice, slice.size) + if ret == -1 && Errno.value == Errno::EAGAIN + wait_readable(io, timeout) + check_open(io) + else + return ret.to_i + end + end + end + + private def evented_write(io, slice : Bytes, timeout : Time::Span?) : Int32 + loop do + ret = LibC.write(io.fd, slice, slice.size) + if ret == -1 && Errno.value == Errno::EAGAIN + wait_writable(io, timeout) + check_open(io) + else + return ret.to_i + end + end + end + + protected def evented_close(io) + return unless (index = io.__evloop_data).valid? + + Evented.arena.free(index) do |pd| + pd.value.@readers.ready_all do |event| + pd.value.@event_loop.try(&.unsafe_resume_io(event)) + end + + pd.value.@writers.ready_all do |event| + pd.value.@event_loop.try(&.unsafe_resume_io(event)) + end + + pd.value.remove(io.fd) + end + end + + private def internal_remove(io) + return unless (index = io.__evloop_data).valid? + + Evented.arena.free(index) do |pd| + pd.value.remove(io.fd) { } # ignore system error + end + end + + private def wait_readable(io, timeout = nil) : Nil + wait_readable(io, timeout) do + raise IO::TimeoutError.new("Read timed out") + end + end + + private def wait_writable(io, timeout = nil) : Nil + wait_writable(io, timeout) do + raise IO::TimeoutError.new("Write timed out") + end + end + + private def wait_readable(io, timeout = nil, &) : Nil + yield if wait(:io_read, io, timeout) do |pd, event| + # don't wait if the waiter has already been marked ready (see Waiters#add) + return unless pd.value.@readers.add(event) + end + end + + private def wait_writable(io, timeout = nil, &) : Nil + yield if wait(:io_write, io, timeout) do |pd, event| + # don't wait if the waiter has already been marked ready (see Waiters#add) + return unless pd.value.@writers.add(event) + end + end + + private def wait(type : Evented::Event::Type, io, timeout, &) + # prepare event (on the stack); we can't initialize it properly until we get + # the arena index below; we also can't use a nilable since `pointerof` would + # point to the union, not the event + event = uninitialized Evented::Event + + # add the event to the waiting list; in case we can't access or allocate the + # poll descriptor into the arena, we merely return to let the caller handle + # the situation (maybe the IO got closed?) + if (index = io.__evloop_data).valid? + event = Evented::Event.new(type, Fiber.current, index, timeout) + + return false unless Evented.arena.get?(index) do |pd| + yield pd, pointerof(event) + end + else + # OPTIMIZE: failing to allocate may be a simple conflict with 2 fibers + # starting to read or write on the same fd, we may want to detect any + # error situation instead of returning and retrying a syscall + return false unless Evented.arena.allocate_at?(io.fd) do |pd, index| + # register the fd with the event loop (once), it should usually merely add + # the fd to the current evloop but may "transfer" the ownership from + # another event loop: + io.__evloop_data = index + pd.value.take_ownership(self, io.fd, index) + + event = Evented::Event.new(type, Fiber.current, index, timeout) + yield pd, pointerof(event) + end + end + + if event.wake_at? + add_timer(pointerof(event)) + + Fiber.suspend + + # no need to delete the timer: either it triggered which means it was + # dequeued, or `#unsafe_resume_io` was called to resume the IO and the + # timer got deleted from the timers before the fiber got reenqueued. + return event.timed_out? + end + + Fiber.suspend + false + end + + private def check_open(io : IO) + raise IO::Error.new("Closed stream") if io.closed? + end + + # internals: timers + + protected def add_timer(event : Evented::Event*) + @lock.sync do + is_next_ready = @timers.add(event) + system_set_timer(event.value.wake_at) if is_next_ready + end + end + + protected def delete_timer(event : Evented::Event*) : Bool + @lock.sync do + dequeued, was_next_ready = @timers.delete(event) + # update system timer if we deleted the next timer + system_set_timer(@timers.next_ready?) if was_next_ready + dequeued + end + end + + # Helper to resume the fiber associated to an IO event and remove the event + # from timers if applicable. Returns true if the fiber has been enqueued. + # + # Thread unsafe: we must hold the poll descriptor waiter lock for the whole + # duration of the dequeue/resume_io otherwise we might conflict with timers + # trying to cancel an IO event. + protected def unsafe_resume_io(event : Evented::Event*) : Bool + # we only partially own the poll descriptor; thanks to the lock we know that + # another thread won't dequeue it, yet it may still be in the timers queue, + # which at worst may be waiting on the lock to be released, so event* can be + # dereferenced safely. + + if !event.value.wake_at? || delete_timer(event) + # no timeout or we canceled it: we fully own the event + Crystal::Scheduler.enqueue(event.value.fiber) + true + else + # failed to cancel the timeout so the timer owns the event (by rule) + false + end + end + + # Process ready timers. + # + # Shall be called after processing IO events. IO events with a timeout that + # have succeeded shall already have been removed from `@timers` otherwise the + # fiber could be resumed twice! + private def process_timers(timer_triggered : Bool) : Nil + # collect ready timers before processing them —this is safe— to avoids a + # deadlock situation when another thread tries to process a ready IO event + # (in poll descriptor waiters) with a timeout (same event* in timers) + buffer = uninitialized StaticArray(Pointer(Evented::Event), 128) + size = 0 + + @lock.sync do + @timers.dequeue_ready do |event| + buffer.to_unsafe[size] = event + break if (size &+= 1) == buffer.size + end + + if size > 0 || timer_triggered + system_set_timer(@timers.next_ready?) + end + end + + buffer.to_slice[0, size].each do |event| + process_timer(event) + end + end + + private def process_timer(event : Evented::Event*) + # we dequeued the event from timers, and by rule we own it, so event* can + # safely be dereferenced: + fiber = event.value.fiber + + case event.value.type + when .io_read? + # reached read timeout: cancel io event; by rule the timer always wins, + # even in case of conflict with #unsafe_resume_io we must resume the fiber + Evented.arena.get?(event.value.index) { |pd| pd.value.@readers.delete(event) } + event.value.timed_out! + when .io_write? + # reached write timeout: cancel io event; by rule the timer always wins, + # even in case of conflict with #unsafe_resume_io we must resume the fiber + Evented.arena.get?(event.value.index) { |pd| pd.value.@writers.delete(event) } + event.value.timed_out! + when .select_timeout? + # always dequeue the event but only enqueue the fiber if we win the + # atomic CAS + return unless select_action = fiber.timeout_select_action + fiber.timeout_select_action = nil + return unless select_action.time_expired? + fiber.@timeout_event.as(FiberEvent).clear + when .sleep? + # cleanup + fiber.@resume_event.as(FiberEvent).clear + else + raise RuntimeError.new("BUG: unexpected event in timers: #{event.value}%s\n") + end + + Crystal::Scheduler.enqueue(fiber) + end + + # internals: system + + # Process ready events and timers. + # + # The loop must always process ready events and timers before returning. When + # *blocking* is `true` the loop must wait for events to become ready (possibly + # indefinitely); when `false` the loop shall return immediately. + # + # The `PollDescriptor` of IO events can be retrieved using the *index* + # from the system event's user data. + private abstract def system_run(blocking : Bool) : Nil + + # Add *fd* to the polling system, setting *index* as user data. + protected abstract def system_add(fd : Int32, index : Index) : Nil + + # Remove *fd* from the polling system. Must raise a `RuntimeError` on error. + # + # If *closing* is true, then it preceeds a call to `close(2)`. Some + # implementations may take advantage of close doing the book keeping. + # + # If *closing* is false then the fd must be deleted from the polling system. + protected abstract def system_del(fd : Int32, closing = true) : Nil + + # Identical to `#system_del` but yields on error. + protected abstract def system_del(fd : Int32, closing = true, &) : Nil + + # Arm a timer to interrupt a run at *time*. Set to `nil` to disarm the timer. + private abstract def system_set_timer(time : Time::Span?) : Nil +end diff --git a/src/crystal/system/unix/evented/fiber_event.cr b/src/crystal/system/unix/evented/fiber_event.cr new file mode 100644 index 000000000000..074dd67e926f --- /dev/null +++ b/src/crystal/system/unix/evented/fiber_event.cr @@ -0,0 +1,33 @@ +class Crystal::Evented::FiberEvent + include Crystal::EventLoop::Event + + def initialize(@event_loop : EventLoop, fiber : Fiber, type : Evented::Event::Type) + @event = Evented::Event.new(type, fiber) + end + + # sleep or select timeout + def add(timeout : Time::Span) : Nil + seconds, nanoseconds = System::Time.monotonic + now = Time::Span.new(seconds: seconds, nanoseconds: nanoseconds) + @event.wake_at = now + timeout + @event_loop.add_timer(pointerof(@event)) + end + + # select timeout has been cancelled + def delete : Nil + return unless @event.wake_at? + + @event.wake_at = nil + @event_loop.delete_timer(pointerof(@event)) + end + + # fiber died + def free : Nil + delete + end + + # the timer triggered (already dequeued from eventloop) + def clear : Nil + @event.wake_at = nil + end +end diff --git a/src/crystal/system/unix/evented/poll_descriptor.cr b/src/crystal/system/unix/evented/poll_descriptor.cr new file mode 100644 index 000000000000..1ef318e454bb --- /dev/null +++ b/src/crystal/system/unix/evented/poll_descriptor.cr @@ -0,0 +1,50 @@ +require "./event_loop" + +# Information related to the evloop for a fd, such as the read and write queues +# (waiting `Event`), as well as which evloop instance currently owns the fd. +# +# Thread-unsafe: parallel mutations must be protected with a lock. +struct Crystal::Evented::PollDescriptor + @event_loop : Evented::EventLoop? + @readers = Waiters.new + @writers = Waiters.new + + # Makes *event_loop* the new owner of *fd*. + # Removes *fd* from the current event loop (if any). + def take_ownership(event_loop : EventLoop, fd : Int32, index : Arena::Index) : Nil + current = @event_loop + + if event_loop == current + raise "BUG: evloop already owns the poll-descriptor for fd=#{fd}" + end + + # ensure we can't have cross enqueues after we transfer the fd, so we + # can optimize (all enqueues are local) and we don't end up with a timer + # from evloop A to cancel an event from evloop B (currently unsafe) + if current && !empty? + raise RuntimeError.new("BUG: transfering fd=#{fd} to another evloop with pending reader/writer fibers") + end + + @event_loop = event_loop + event_loop.system_add(fd, index) + current.try(&.system_del(fd, closing: false)) + end + + # Removes *fd* from its owner event loop. Raises on errors. + def remove(fd : Int32) : Nil + current, @event_loop = @event_loop, nil + current.try(&.system_del(fd)) + end + + # Same as `#remove` but yields on errors. + def remove(fd : Int32, &) : Nil + current, @event_loop = @event_loop, nil + current.try(&.system_del(fd) { yield }) + end + + # Returns true when there is at least one reader or writer. Returns false + # otherwise. + def empty? : Bool + @readers.@list.empty? && @writers.@list.empty? + end +end diff --git a/src/crystal/system/unix/evented/timers.cr b/src/crystal/system/unix/evented/timers.cr new file mode 100644 index 000000000000..ace4fefcf09b --- /dev/null +++ b/src/crystal/system/unix/evented/timers.cr @@ -0,0 +1,86 @@ +# List of `Event` ordered by `Event#wake_at` ascending. Optimized for fast +# dequeue and determining when is the next timer event. +# +# Thread unsafe: parallel accesses much be protected. +# +# NOTE: this is a struct because it only wraps a const pointer to a deque +# allocated in the heap. +# +# OPTIMIZE: consider a skiplist for faster lookups (add/delete). +# +# OPTIMIZE: we could avoid memmove on add/delete by allocating a buffer, putting +# entries at whatever available index in the buffer, and linking entries in +# order (using indices so we can realloc the buffer); we'd have to keep a list +# of free indexes, too. It could be a good combo of unbounded linked list while +# retaining some memory locality. It should even be compatible with a skiplist +# (e.g. make entries a fixed height tower instead of prev/next node). +struct Crystal::Evented::Timers + def initialize + @list = Deque(Evented::Event*).new + end + + def empty? : Bool + @list.empty? + end + + # Returns the time at which the next timer is supposed to run. + def next_ready? : Time::Span? + @list.first?.try(&.value.wake_at) + end + + # Dequeues and yields each ready timer (their `#wake_at` is lower than + # `System::Time.monotonic`) from the oldest to the most recent (i.e. time + # ascending). + def dequeue_ready(& : Evented::Event* -> Nil) : Nil + return if @list.empty? + + seconds, nanoseconds = System::Time.monotonic + now = Time::Span.new(seconds: seconds, nanoseconds: nanoseconds) + n = 0 + + @list.each do |event| + break if event.value.wake_at > now + yield event + n += 1 + end + + # OPTIMIZE: consume the n entries at once + n.times { @list.shift } + end + + # Add a new timer into the list. Returns true if it is the next ready timer. + def add(event : Evented::Event*) : Bool + if @list.empty? + @list << event + true + elsif index = lookup(event.value.wake_at) + @list.insert(index, event) + index == 0 + else + @list.push(event) + false + end + end + + private def lookup(wake_at) + @list.each_with_index do |event, index| + return index if event.value.wake_at >= wake_at + end + end + + # Remove a timer from the list. Returns a tuple(dequeued, was_next_ready) of + # booleans. The first bool tells whether the event was dequeued, in which case + # the second one tells if it was the next ready event. + def delete(event : Evented::Event*) : {Bool, Bool} + if index = @list.index(event) + @list.delete_at(index) + {true, index.zero?} + else + {false, false} + end + end + + def each(&) : Nil + @list.each { |event| yield event } + end +end diff --git a/src/crystal/system/unix/evented/waiters.cr b/src/crystal/system/unix/evented/waiters.cr new file mode 100644 index 000000000000..2d052718bae9 --- /dev/null +++ b/src/crystal/system/unix/evented/waiters.cr @@ -0,0 +1,62 @@ +require "./event" + +# A FIFO queue of `Event` waiting on the same operation (either read or write) +# for a fd. See `PollDescriptor`. +# +# Race conditions on the state of the waiting list are handled through the ready +# always ready variables. +# +# Thread unsafe: parallel mutations must be protected with a lock. +struct Crystal::Evented::Waiters + @list = PointerLinkedList(Event).new + @ready = false + @always_ready = false + + # Adds an event to the waiting list. May return false immediately if another + # thread marked the list as ready in parallel, returns true otherwise. + def add(event : Pointer(Event)) : Bool + if @always_ready + # another thread closed the fd or we received a fd error or hup event: + # the fd will never block again + return false + end + + if @ready + # another thread readied the fd before the current thread got to add + # the event: don't block and resets @ready for the next loop + @ready = false + return false + end + + @list.push(event) + true + end + + def delete(event : Pointer(Event)) : Nil + @list.delete(event) if event.value.next + end + + # Removes one pending event or marks the list as ready when there are no + # pending events (we got notified of readiness before a thread enqueued). + def ready_one(& : Pointer(Event) -> Bool) : Nil + # loop until the block succesfully processes an event (it may have to + # dequeue the timeout from timers) + loop do + if event = @list.shift? + break if yield event + else + # no event queued but another thread may be waiting for the lock to + # add an event: set as ready to resolve the race condition + @ready = true + return + end + end + end + + # Dequeues all pending events and marks the list as always ready. This must be + # called when a fd is closed or an error or hup event occurred. + def ready_all(& : Pointer(Event) ->) : Nil + @list.consume_each { |event| yield event } + @always_ready = true + end +end diff --git a/src/crystal/system/unix/eventfd.cr b/src/crystal/system/unix/eventfd.cr new file mode 100644 index 000000000000..6180bf90bf23 --- /dev/null +++ b/src/crystal/system/unix/eventfd.cr @@ -0,0 +1,31 @@ +require "c/sys/eventfd" + +struct Crystal::System::EventFD + # NOTE: no need to concern ourselves with endianness: we interpret the bytes + # in the system order and eventfd can only be used locally (no cross system + # issues). + + getter fd : Int32 + + def initialize(value = 0) + @fd = LibC.eventfd(value, LibC::EFD_CLOEXEC) + raise RuntimeError.from_errno("eventfd") if @fd == -1 + end + + def read : UInt64 + buf = uninitialized UInt8[8] + bytes_read = LibC.read(@fd, buf.to_unsafe, buf.size) + raise RuntimeError.from_errno("eventfd_read") unless bytes_read == 8 + buf.unsafe_as(UInt64) + end + + def write(value : UInt64) : Nil + buf = value.unsafe_as(StaticArray(UInt8, 8)) + bytes_written = LibC.write(@fd, buf.to_unsafe, buf.size) + raise RuntimeError.from_errno("eventfd_write") unless bytes_written == 8 + end + + def close : Nil + LibC.close(@fd) + end +end diff --git a/src/crystal/system/unix/file_descriptor.cr b/src/crystal/system/unix/file_descriptor.cr index 60515b701136..4aa1ec580d32 100644 --- a/src/crystal/system/unix/file_descriptor.cr +++ b/src/crystal/system/unix/file_descriptor.cr @@ -1,5 +1,4 @@ require "c/fcntl" -require "io/evented" require "termios" {% if flag?(:android) && LibC::ANDROID_API < 28 %} require "c/sys/ioctl" @@ -7,7 +6,9 @@ require "termios" # :nodoc: module Crystal::System::FileDescriptor - include IO::Evented + {% if IO.has_constant?(:Evented) %} + include IO::Evented + {% end %} # Platform-specific type to represent a file descriptor handle to the operating # system. diff --git a/src/crystal/system/unix/kqueue.cr b/src/crystal/system/unix/kqueue.cr new file mode 100644 index 000000000000..9f7cb1f414b9 --- /dev/null +++ b/src/crystal/system/unix/kqueue.cr @@ -0,0 +1,89 @@ +require "c/sys/event" + +struct Crystal::System::Kqueue + @kq : LibC::Int + + def initialize + @kq = + {% if LibC.has_method?(:kqueue1) %} + LibC.kqueue1(LibC::O_CLOEXEC) + {% else %} + LibC.kqueue + {% end %} + if @kq == -1 + function_name = {% if LibC.has_method?(:kqueue1) %} "kqueue1" {% else %} "kqueue" {% end %} + raise RuntimeError.from_errno(function_name) + end + end + + # Helper to register a single event. Returns immediately. + def kevent(ident, filter, flags, fflags = 0, data = 0, udata = nil, &) : Nil + kevent = uninitialized LibC::Kevent + Kqueue.set pointerof(kevent), ident, filter, flags, fflags, data, udata + ret = LibC.kevent(@kq, pointerof(kevent), 1, nil, 0, nil) + yield if ret == -1 + end + + # Helper to register a single event. Returns immediately. + def kevent(ident, filter, flags, fflags = 0, data = 0, udata = nil) : Nil + kevent(ident, filter, flags, fflags, data, udata) do + raise RuntimeError.from_errno("kevent") + end + end + + # Helper to register multiple *changes*. Returns immediately. + def kevent(changes : Slice(LibC::Kevent), &) : Nil + ret = LibC.kevent(@kq, changes.to_unsafe, changes.size, nil, 0, nil) + yield if ret == -1 + end + + # Waits for registered events to become active. Returns a subslice to + # *events*. + # + # Timeout is relative to now; blocks indefinitely if `nil`; returns + # immediately if zero. + def wait(events : Slice(LibC::Kevent), timeout : ::Time::Span? = nil) : Slice(LibC::Kevent) + if timeout + ts = uninitialized LibC::Timespec + ts.tv_sec = typeof(ts.tv_sec).new!(timeout.@seconds) + ts.tv_nsec = typeof(ts.tv_nsec).new!(timeout.@nanoseconds) + tsp = pointerof(ts) + else + tsp = Pointer(LibC::Timespec).null + end + + changes = Slice(LibC::Kevent).empty + count = 0 + + loop do + count = LibC.kevent(@kq, changes.to_unsafe, changes.size, events.to_unsafe, events.size, tsp) + break unless count == -1 + + if Errno.value == Errno::EINTR + # retry when waiting indefinitely, return otherwise + break if timeout + else + raise RuntimeError.from_errno("kevent") + end + end + + events[0, count.clamp(0..)] + end + + def close : Nil + LibC.close(@kq) + end + + @[AlwaysInline] + def self.set(kevent : LibC::Kevent*, ident, filter, flags, fflags = 0, data = 0, udata = nil) : Nil + kevent.value.ident = ident + kevent.value.filter = filter + kevent.value.flags = flags + kevent.value.fflags = fflags + kevent.value.data = data + kevent.value.udata = udata ? udata.as(Void*) : Pointer(Void).null + {% if LibC::Kevent.has_method?(:ext) %} + kevent.value.ext.fill(0) + {% end %} + end +end diff --git a/src/crystal/system/unix/kqueue/event_loop.cr b/src/crystal/system/unix/kqueue/event_loop.cr new file mode 100644 index 000000000000..6eb98a7dc948 --- /dev/null +++ b/src/crystal/system/unix/kqueue/event_loop.cr @@ -0,0 +1,245 @@ +require "../evented/event_loop" +require "../kqueue" + +class Crystal::Kqueue::EventLoop < Crystal::Evented::EventLoop + # the following are arbitrary numbers to identify specific events + INTERRUPT_IDENTIFIER = 9 + TIMER_IDENTIFIER = 10 + + {% unless LibC.has_constant?(:EVFILT_USER) %} + @pipe = uninitialized LibC::Int[2] + {% end %} + + def initialize + # the kqueue instance + @kqueue = System::Kqueue.new + + # notification to interrupt a run + @interrupted = Atomic::Flag.new + + {% if LibC.has_constant?(:EVFILT_USER) %} + @kqueue.kevent( + INTERRUPT_IDENTIFIER, + LibC::EVFILT_USER, + LibC::EV_ADD | LibC::EV_ENABLE | LibC::EV_CLEAR) + {% else %} + @pipe = System::FileDescriptor.system_pipe + @kqueue.kevent(@pipe[0], LibC::EVFILT_READ, LibC::EV_ADD) + {% end %} + end + + def after_fork_before_exec : Nil + super + + # O_CLOEXEC would close these automatically but we don't want to mess with + # the parent process fds (that would mess the parent evloop) + + # kqueue isn't inherited by fork on darwin/dragonfly, but we still close + @kqueue.close + + {% unless LibC.has_constant?(:EVFILT_USER) %} + @pipe.each { |fd| LibC.close(fd) } + {% end %} + end + + {% unless flag?(:preview_mt) %} + def after_fork : Nil + super + + # kqueue isn't inherited by fork on darwin/dragonfly, but we still close + @kqueue.close + @kqueue = System::Kqueue.new + + @interrupted.clear + + {% if LibC.has_constant?(:EVFILT_USER) %} + @kqueue.kevent( + INTERRUPT_IDENTIFIER, + LibC::EVFILT_USER, + LibC::EV_ADD | LibC::EV_ENABLE | LibC::EV_CLEAR) + {% else %} + @pipe.each { |fd| LibC.close(fd) } + @pipe = System::FileDescriptor.system_pipe + @kqueue.kevent(@pipe[0], LibC::EVFILT_READ, LibC::EV_ADD) + {% end %} + + system_set_timer(@timers.next_ready?) + + # re-add all registered fds + Evented.arena.each { |fd, index| system_add(fd, index) } + end + {% end %} + + private def system_run(blocking : Bool) : Nil + buffer = uninitialized LibC::Kevent[128] + + Crystal.trace :evloop, "run", blocking: blocking ? 1 : 0 + timeout = blocking ? nil : Time::Span.zero + kevents = @kqueue.wait(buffer.to_slice, timeout) + + timer_triggered = false + + # process events + kevents.size.times do |i| + kevent = kevents.to_unsafe + i + + if process_interrupt?(kevent) + # nothing special + elsif kevent.value.filter == LibC::EVFILT_TIMER + # nothing special + timer_triggered = true + else + process_io(kevent) + end + end + + process_timers(timer_triggered) + end + + private def process_interrupt?(kevent) + {% if LibC.has_constant?(:EVFILT_USER) %} + if kevent.value.filter == LibC::EVFILT_USER + @interrupted.clear if kevent.value.ident == INTERRUPT_IDENTIFIER + return true + end + {% else %} + if kevent.value.filter == LibC::EVFILT_READ && kevent.value.ident == @pipe[0] + ident = 0 + ret = LibC.read(@pipe[0], pointerof(ident), sizeof(Int32)) + raise RuntimeError.from_errno("read") if ret == -1 + @interrupted.clear if ident == INTERRUPT_IDENTIFIER + return true + end + {% end %} + false + end + + private def process_io(kevent : LibC::Kevent*) : Nil + index = + {% if flag?(:bits64) %} + Evented::Arena::Index.new(kevent.value.udata.address) + {% else %} + # assuming 32-bit target: rebuild the arena index + Evented::Arena::Index.new(kevent.value.ident.to_i32!, kevent.value.udata.address.to_u32!) + {% end %} + + Crystal.trace :evloop, "event", fd: kevent.value.ident, index: index.to_i64, + filter: kevent.value.filter, flags: kevent.value.flags, fflags: kevent.value.fflags + + Evented.arena.get?(index) do |pd| + if (kevent.value.fflags & LibC::EV_EOF) == LibC::EV_EOF + # apparently some systems may report EOF on write with EVFILT_READ instead + # of EVFILT_WRITE, so let's wake all waiters: + pd.value.@readers.ready_all { |event| unsafe_resume_io(event) } + pd.value.@writers.ready_all { |event| unsafe_resume_io(event) } + return + end + + case kevent.value.filter + when LibC::EVFILT_READ + if (kevent.value.fflags & LibC::EV_ERROR) == LibC::EV_ERROR + # OPTIMIZE: pass errno (kevent.data) through PollDescriptor + pd.value.@readers.ready_all { |event| unsafe_resume_io(event) } + else + pd.value.@readers.ready_one { |event| unsafe_resume_io(event) } + end + when LibC::EVFILT_WRITE + if (kevent.value.fflags & LibC::EV_ERROR) == LibC::EV_ERROR + # OPTIMIZE: pass errno (kevent.data) through PollDescriptor + pd.value.@writers.ready_all { |event| unsafe_resume_io(event) } + else + pd.value.@writers.ready_one { |event| unsafe_resume_io(event) } + end + end + end + end + + def interrupt : Nil + return unless @interrupted.test_and_set + + {% if LibC.has_constant?(:EVFILT_USER) %} + @kqueue.kevent(INTERRUPT_IDENTIFIER, LibC::EVFILT_USER, 0, LibC::NOTE_TRIGGER) + {% else %} + ident = INTERRUPT_IDENTIFIER + ret = LibC.write(@pipe[1], pointerof(ident), sizeof(Int32)) + raise RuntimeError.from_errno("write") if ret == -1 + {% end %} + end + + protected def system_add(fd : Int32, index : Evented::Arena::Index) : Nil + Crystal.trace :evloop, "kevent", op: "add", fd: fd, index: index.to_i64 + + # register both read and write events + kevents = uninitialized LibC::Kevent[2] + {LibC::EVFILT_READ, LibC::EVFILT_WRITE}.each_with_index do |filter, i| + kevent = kevents.to_unsafe + i + udata = + {% if flag?(:bits64) %} + Pointer(Void).new(index.to_u64) + {% else %} + # assuming 32-bit target: pass the generation as udata (ident is the fd/index) + Pointer(Void).new(index.generation) + {% end %} + System::Kqueue.set(kevent, fd, filter, LibC::EV_ADD | LibC::EV_CLEAR, udata: udata) + end + + @kqueue.kevent(kevents.to_slice) do + raise RuntimeError.from_errno("kevent") + end + end + + protected def system_del(fd : Int32, closing = true) : Nil + system_del(fd, closing) do + raise RuntimeError.from_errno("kevent") + end + end + + protected def system_del(fd : Int32, closing = true, &) : Nil + return if closing # nothing to do: close(2) will do the cleanup + + Crystal.trace :evloop, "kevent", op: "del", fd: fd + + # unregister both read and write events + kevents = uninitialized LibC::Kevent[2] + {LibC::EVFILT_READ, LibC::EVFILT_WRITE}.each_with_index do |filter, i| + kevent = kevents.to_unsafe + i + System::Kqueue.set(kevent, fd, filter, LibC::EV_DELETE) + end + + @kqueue.kevent(kevents.to_slice) do + raise RuntimeError.from_errno("kevent") + end + end + + private def system_set_timer(time : Time::Span?) : Nil + if time + flags = LibC::EV_ADD | LibC::EV_ONESHOT | LibC::EV_CLEAR + + seconds, nanoseconds = System::Time.monotonic + now = Time::Span.new(seconds: seconds, nanoseconds: nanoseconds) + t = time - now + + data = + {% if LibC.has_constant?(:NOTE_NSECONDS) %} + t.total_nanoseconds.to_i64!.clamp(0..) + {% else %} + # legacy BSD (and DragonFly) only have millisecond precision + t.positive? ? t.total_milliseconds.to_i64!.clamp(1..) : 0 + {% end %} + else + flags = LibC::EV_DELETE + data = 0_u64 + end + + fflags = + {% if LibC.has_constant?(:NOTE_NSECONDS) %} + LibC::NOTE_NSECONDS + {% else %} + 0 + {% end %} + + @kqueue.kevent(TIMER_IDENTIFIER, LibC::EVFILT_TIMER, flags, fflags, data) do + raise RuntimeError.from_errno("kevent") unless Errno.value == Errno::ENOENT + end + end +end diff --git a/src/crystal/system/unix/lib_event2.cr b/src/crystal/system/unix/lib_event2.cr index 2cd3e4635194..e8e44b0f7473 100644 --- a/src/crystal/system/unix/lib_event2.cr +++ b/src/crystal/system/unix/lib_event2.cr @@ -7,6 +7,11 @@ require "c/netdb" @[Link("rt")] {% end %} +# Supported library versions: +# +# * libevent2 +# +# See https://crystal-lang.org/reference/man/required_libraries.html#other-runtime-libraries {% if flag?(:openbsd) %} @[Link("event_core")] @[Link("event_extra")] diff --git a/src/crystal/system/unix/process.cr b/src/crystal/system/unix/process.cr index 0eb58231900e..875d834bb266 100644 --- a/src/crystal/system/unix/process.cr +++ b/src/crystal/system/unix/process.cr @@ -352,6 +352,7 @@ struct Crystal::System::Process private def self.reopen_io(src_io : IO::FileDescriptor, dst_io : IO::FileDescriptor) if src_io.closed? + Crystal::EventLoop.current.remove(dst_io) dst_io.file_descriptor_close else src_io = to_real_fd(src_io) diff --git a/src/crystal/system/unix/signal.cr b/src/crystal/system/unix/signal.cr index 1d1e885fc71d..802cb418db15 100644 --- a/src/crystal/system/unix/signal.cr +++ b/src/crystal/system/unix/signal.cr @@ -22,17 +22,21 @@ module Crystal::System::Signal @@mutex.synchronize do unless @@handlers[signal]? @@sigset << signal - action = LibC::Sigaction.new - - # restart some interrupted syscalls (read, write, accept, ...) instead - # of returning EINTR: - action.sa_flags = LibC::SA_RESTART - - action.sa_sigaction = LibC::SigactionHandlerT.new do |value, _, _| - writer.write_bytes(value) unless writer.closed? - end - LibC.sigemptyset(pointerof(action.@sa_mask)) - LibC.sigaction(signal, pointerof(action), nil) + {% if flag?(:interpreted) && Crystal::Interpreter.has_method?(:signal) %} + Crystal::Interpreter.signal(signal.value, 2) + {% else %} + action = LibC::Sigaction.new + + # restart some interrupted syscalls (read, write, accept, ...) instead + # of returning EINTR: + action.sa_flags = LibC::SA_RESTART + + action.sa_sigaction = LibC::SigactionHandlerT.new do |value, _, _| + writer.write_bytes(value) unless writer.closed? + end + LibC.sigemptyset(pointerof(action.@sa_mask)) + LibC.sigaction(signal, pointerof(action), nil) + {% end %} end @@handlers[signal] = handler end @@ -62,7 +66,16 @@ module Crystal::System::Signal else @@mutex.synchronize do @@handlers.delete(signal) - LibC.signal(signal, handler) + {% if flag?(:interpreted) && Crystal::Interpreter.has_method?(:signal) %} + h = case handler + when LibC::SIG_DFL then 0 + when LibC::SIG_IGN then 1 + else 2 + end + Crystal::Interpreter.signal(signal.value, h) + {% else %} + LibC.signal(signal, handler) + {% end %} @@sigset.delete(signal) end end @@ -97,7 +110,10 @@ module Crystal::System::Signal # Replaces the signal pipe so the child process won't share the file # descriptors of the parent process and send it received signals. def self.after_fork - @@pipe.each(&.file_descriptor_close) + @@pipe.each do |pipe_io| + Crystal::EventLoop.current.remove(pipe_io) + pipe_io.file_descriptor_close { } + end ensure @@pipe = IO.pipe(read_blocking: false, write_blocking: true) end @@ -116,7 +132,13 @@ module Crystal::System::Signal # sub-process. def self.after_fork_before_exec ::Signal.each do |signal| - LibC.signal(signal, LibC::SIG_DFL) if @@sigset.includes?(signal) + next unless @@sigset.includes?(signal) + + {% if flag?(:interpreted) && Crystal::Interpreter.has_method?(:signal) %} + Crystal::Interpreter.signal(signal.value, 0) + {% else %} + LibC.signal(signal, LibC::SIG_DFL) + {% end %} end ensure {% unless flag?(:preview_mt) %} @@ -132,6 +154,14 @@ module Crystal::System::Signal @@pipe[1] end + {% unless flag?(:interpreted) %} + # :nodoc: + def self.writer=(writer : IO::FileDescriptor) + @@pipe = {@@pipe[0], writer} + writer + end + {% end %} + private def self.fatal(message : String) STDERR.puts("FATAL: #{message}, exiting") STDERR.flush @@ -175,7 +205,16 @@ module Crystal::System::Signal return unless @@setup_default_handlers.test_and_set @@sigset.clear start_loop - ::Signal::PIPE.ignore + + {% if flag?(:interpreted) && Interpreter.has_method?(:signal_descriptor) %} + # replace the interpreter's writer pipe with the interpreted, so signals + # will be received by the interpreter, but handled by the interpreted + # signal loop + Crystal::Interpreter.signal_descriptor(@@pipe[1].fd) + {% else %} + ::Signal::PIPE.ignore + {% end %} + ::Signal::CHLD.reset end diff --git a/src/crystal/system/unix/socket.cr b/src/crystal/system/unix/socket.cr index 7c39e140849c..535f37f386c0 100644 --- a/src/crystal/system/unix/socket.cr +++ b/src/crystal/system/unix/socket.cr @@ -1,10 +1,11 @@ require "c/netdb" require "c/netinet/tcp" require "c/sys/socket" -require "io/evented" module Crystal::System::Socket - include IO::Evented + {% if IO.has_constant?(:Evented) %} + include IO::Evented + {% end %} alias Handle = Int32 @@ -24,6 +25,9 @@ module Crystal::System::Socket end private def initialize_handle(fd) + {% if Crystal.has_constant?(:Evented) %} + @__evloop_data = Crystal::Evented::Arena::INVALID_INDEX + {% end %} end # Tries to bind the socket to a local address. diff --git a/src/crystal/system/unix/timerfd.cr b/src/crystal/system/unix/timerfd.cr new file mode 100644 index 000000000000..34edbbec7482 --- /dev/null +++ b/src/crystal/system/unix/timerfd.cr @@ -0,0 +1,33 @@ +require "c/sys/timerfd" + +struct Crystal::System::TimerFD + getter fd : Int32 + + # Create a `timerfd` instance set to the monotonic clock. + def initialize + @fd = LibC.timerfd_create(LibC::CLOCK_MONOTONIC, LibC::TFD_CLOEXEC) + raise RuntimeError.from_errno("timerfd_settime") if @fd == -1 + end + + # Arm (start) the timer to run at *time* (absolute time). + def set(time : ::Time::Span) : Nil + itimerspec = uninitialized LibC::Itimerspec + itimerspec.it_interval.tv_sec = 0 + itimerspec.it_interval.tv_nsec = 0 + itimerspec.it_value.tv_sec = typeof(itimerspec.it_value.tv_sec).new!(time.@seconds) + itimerspec.it_value.tv_nsec = typeof(itimerspec.it_value.tv_nsec).new!(time.@nanoseconds) + ret = LibC.timerfd_settime(@fd, LibC::TFD_TIMER_ABSTIME, pointerof(itimerspec), nil) + raise RuntimeError.from_errno("timerfd_settime") if ret == -1 + end + + # Disarm (stop) the timer. + def cancel : Nil + itimerspec = LibC::Itimerspec.new + ret = LibC.timerfd_settime(@fd, LibC::TFD_TIMER_ABSTIME, pointerof(itimerspec), nil) + raise RuntimeError.from_errno("timerfd_settime") if ret == -1 + end + + def close + LibC.close(@fd) + end +end diff --git a/src/crystal/system/win32/file_descriptor.cr b/src/crystal/system/win32/file_descriptor.cr index 1f277505302a..4265701cd8b2 100644 --- a/src/crystal/system/win32/file_descriptor.cr +++ b/src/crystal/system/win32/file_descriptor.cr @@ -195,7 +195,11 @@ module Crystal::System::FileDescriptor end def file_descriptor_close(&) - if LibC.CloseHandle(windows_handle) == 0 + # Clear the @volatile_fd before actually closing it in order to + # reduce the chance of reading an outdated handle value + handle = LibC::HANDLE.new(@volatile_fd.swap(LibC::INVALID_HANDLE_VALUE.address)) + + if LibC.CloseHandle(handle) == 0 yield end end diff --git a/src/crystal/system/win32/wmain.cr b/src/crystal/system/win32/wmain.cr index caad6748229f..2120bfc06bfc 100644 --- a/src/crystal/system/win32/wmain.cr +++ b/src/crystal/system/win32/wmain.cr @@ -2,14 +2,12 @@ require "c/stringapiset" require "c/winnls" require "c/stdlib" -{% begin %} - # we have both `main` and `wmain`, so we must choose an unambiguous entry point +# we have both `main` and `wmain`, so we must choose an unambiguous entry point +{% if flag?(:msvc) %} @[Link({{ flag?(:static) ? "libcmt" : "msvcrt" }})] - {% if flag?(:msvc) %} - @[Link(ldflags: "/ENTRY:wmainCRTStartup")] - {% elsif flag?(:gnu) && !flag?(:interpreted) %} - @[Link(ldflags: "-municode")] - {% end %} + @[Link(ldflags: "/ENTRY:wmainCRTStartup")] +{% elsif flag?(:gnu) && !flag?(:interpreted) %} + @[Link(ldflags: "-municode")] {% end %} lib LibCrystalMain end diff --git a/src/crystal/tracing.cr b/src/crystal/tracing.cr index a680bfea717f..684680b10b28 100644 --- a/src/crystal/tracing.cr +++ b/src/crystal/tracing.cr @@ -7,6 +7,7 @@ module Crystal enum Section GC Sched + Evloop def self.from_id(slice) : self {% begin %} diff --git a/src/dir/glob.cr b/src/dir/glob.cr index cd45f0a03baf..2fc8d988c20a 100644 --- a/src/dir/glob.cr +++ b/src/dir/glob.cr @@ -37,6 +37,10 @@ class Dir # Returns an array of all files that match against any of *patterns*. # + # ``` + # Dir.glob "path/to/folder/*.txt" # Returns all files in the target folder that end in ".txt". + # Dir.glob "path/to/folder/**/*" # Returns all files in the target folder and its subfolders. + # ``` # The pattern syntax is similar to shell filename globbing, see `File.match?` for details. # # NOTE: Path separator in patterns needs to be always `/`. The returned file names use system-specific path separators. diff --git a/src/empty.cr b/src/empty.cr index 204e30da48c0..cb79610a5be3 100644 --- a/src/empty.cr +++ b/src/empty.cr @@ -1,6 +1,6 @@ require "primitives" -{% if flag?(:win32) %} +{% if flag?(:msvc) %} @[Link({{ flag?(:static) ? "libcmt" : "msvcrt" }})] # For `mainCRTStartup` {% end %} lib LibCrystalMain diff --git a/src/errno.cr b/src/errno.cr index 9d608c80bc1b..c519a8ab9fdb 100644 --- a/src/errno.cr +++ b/src/errno.cr @@ -38,7 +38,10 @@ enum Errno {% end %} {% end %} - # Convert an Errno to an error message + # Returns the system error message associated with this errno. + # + # NOTE: The result may depend on the current system locale. Specs and + # comparisons should use `#value` instead of this method. def message : String unsafe_message { |slice| String.new(slice) } end diff --git a/src/fiber.cr b/src/fiber.cr index 1086ebdd3669..2b596a16017c 100644 --- a/src/fiber.cr +++ b/src/fiber.cr @@ -246,12 +246,15 @@ class Fiber @timeout_event.try &.delete end + # :nodoc: + # # The current fiber will resume after a period of time. # The timeout can be cancelled with `cancel_timeout` def self.timeout(timeout : Time::Span, select_action : Channel::TimeoutAction) : Nil Fiber.current.timeout(timeout, select_action) end + # :nodoc: def self.cancel_timeout : Nil Fiber.current.cancel_timeout end diff --git a/src/gc/boehm.cr b/src/gc/boehm.cr index 33d6466d792b..6037abe830e2 100644 --- a/src/gc/boehm.cr +++ b/src/gc/boehm.cr @@ -32,6 +32,11 @@ require "crystal/tracing" @[Link("gc", pkg_config: "bdw-gc")] {% end %} +# Supported library versions: +# +# * libgc (8.2.0+; earlier versions require a patch for MT support) +# +# See https://crystal-lang.org/reference/man/required_libraries.html#other-runtime-libraries {% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %} @[Link(dll: "gc.dll")] {% end %} diff --git a/src/gc/none.cr b/src/gc/none.cr index ce84027e6e69..651027266e5b 100644 --- a/src/gc/none.cr +++ b/src/gc/none.cr @@ -1,5 +1,6 @@ {% if flag?(:win32) %} require "c/process" + require "c/heapapi" {% end %} require "crystal/tracing" @@ -11,21 +12,42 @@ module GC # :nodoc: def self.malloc(size : LibC::SizeT) : Void* Crystal.trace :gc, "malloc", size: size - # libc malloc is not guaranteed to return cleared memory, so we need to - # explicitly clear it. Ref: https://github.com/crystal-lang/crystal/issues/14678 - LibC.malloc(size).tap(&.clear) + + {% if flag?(:win32) %} + LibC.HeapAlloc(LibC.GetProcessHeap, LibC::HEAP_ZERO_MEMORY, size) + {% else %} + # libc malloc is not guaranteed to return cleared memory, so we need to + # explicitly clear it. Ref: https://github.com/crystal-lang/crystal/issues/14678 + LibC.malloc(size).tap(&.clear) + {% end %} end # :nodoc: def self.malloc_atomic(size : LibC::SizeT) : Void* Crystal.trace :gc, "malloc", size: size, atomic: 1 - LibC.malloc(size) + + {% if flag?(:win32) %} + LibC.HeapAlloc(LibC.GetProcessHeap, 0, size) + {% else %} + LibC.malloc(size) + {% end %} end # :nodoc: def self.realloc(pointer : Void*, size : LibC::SizeT) : Void* Crystal.trace :gc, "realloc", size: size - LibC.realloc(pointer, size) + + {% if flag?(:win32) %} + # realloc with a null pointer should behave like plain malloc, but Win32 + # doesn't do that + if pointer + LibC.HeapReAlloc(LibC.GetProcessHeap, LibC::HEAP_ZERO_MEMORY, pointer, size) + else + LibC.HeapAlloc(LibC.GetProcessHeap, LibC::HEAP_ZERO_MEMORY, size) + end + {% else %} + LibC.realloc(pointer, size) + {% end %} end def self.collect @@ -39,7 +61,12 @@ module GC def self.free(pointer : Void*) : Nil Crystal.trace :gc, "free" - LibC.free(pointer) + + {% if flag?(:win32) %} + LibC.HeapFree(LibC.GetProcessHeap, 0, pointer) + {% else %} + LibC.free(pointer) + {% end %} end def self.is_heap_ptr(pointer : Void*) : Bool diff --git a/src/io/evented.cr b/src/io/evented.cr index d2b3a66c336f..f59aa205c543 100644 --- a/src/io/evented.cr +++ b/src/io/evented.cr @@ -1,4 +1,6 @@ -{% skip_file if flag?(:win32) %} +require "crystal/system/event_loop" + +{% skip_file unless flag?(:wasi) || Crystal.has_constant?(:LibEvent) %} require "crystal/thread_local_value" diff --git a/src/kernel.cr b/src/kernel.cr index ac241161c16d..1203d1c66a7e 100644 --- a/src/kernel.cr +++ b/src/kernel.cr @@ -629,3 +629,7 @@ end end end {% end %} + +{% if flag?(:interpreted) && flag?(:unix) && Crystal::Interpreter.has_method?(:signal_descriptor) %} + Crystal::System::Signal.setup_default_handlers +{% end %} diff --git a/src/levenshtein.cr b/src/levenshtein.cr index e890d59c90ef..01ad1bc40784 100644 --- a/src/levenshtein.cr +++ b/src/levenshtein.cr @@ -139,7 +139,7 @@ module Levenshtein # end # best_match # => "ello" # ``` - def self.find(name, tolerance = nil, &) + def self.find(name, tolerance = nil, &) : String? Finder.find(name, tolerance) do |sn| yield sn end @@ -154,7 +154,7 @@ module Levenshtein # Levenshtein.find("hello", ["hullo", "hel", "hall", "hell"], 2) # => "hullo" # Levenshtein.find("hello", ["hurlo", "hel", "hall"], 1) # => nil # ``` - def self.find(name, all_names, tolerance = nil) + def self.find(name, all_names, tolerance = nil) : String? Finder.find(name, all_names, tolerance) end end diff --git a/src/lib_c.cr b/src/lib_c.cr index 0bd8d2c2cc35..c52ea52bfcbc 100644 --- a/src/lib_c.cr +++ b/src/lib_c.cr @@ -1,4 +1,15 @@ -{% if flag?(:win32) %} +# Supported library versions: +# +# * glibc (2.26+) +# * musl libc (1.2+) +# * system libraries of several BSDs +# * macOS system library (11+) +# * MSVCRT +# * WASI +# * bionic libc +# +# See https://crystal-lang.org/reference/man/required_libraries.html#system-library +{% if flag?(:msvc) %} @[Link({{ flag?(:static) ? "libucrt" : "ucrt" }})] {% end %} lib LibC diff --git a/src/lib_c/aarch64-android/c/sys/epoll.cr b/src/lib_c/aarch64-android/c/sys/epoll.cr new file mode 100644 index 000000000000..ba34b8414732 --- /dev/null +++ b/src/lib_c/aarch64-android/c/sys/epoll.cr @@ -0,0 +1,32 @@ +lib LibC + EPOLLIN = 0x001_u32 + EPOLLOUT = 0x004_u32 + EPOLLERR = 0x008_u32 + EPOLLHUP = 0x010_u32 + EPOLLRDHUP = 0x2000_u32 + + EPOLLEXCLUSIVE = 1_u32 << 28 + EPOLLET = 1_u32 << 31 + + EPOLL_CTL_ADD = 1 + EPOLL_CTL_DEL = 2 + EPOLL_CTL_MOD = 3 + + EPOLL_CLOEXEC = 0o2000000 + + union EpollDataT + ptr : Void* + fd : Int + u32 : UInt32 + u64 : UInt64 + end + + struct EpollEvent + events : UInt32 + data : EpollDataT + end + + fun epoll_create1(Int) : Int + fun epoll_ctl(Int, Int, Int, EpollEvent*) : Int + fun epoll_wait(Int, EpollEvent*, Int, Int) : Int +end diff --git a/src/lib_c/aarch64-android/c/sys/eventfd.cr b/src/lib_c/aarch64-android/c/sys/eventfd.cr new file mode 100644 index 000000000000..12f24428a8f4 --- /dev/null +++ b/src/lib_c/aarch64-android/c/sys/eventfd.cr @@ -0,0 +1,5 @@ +lib LibC + EFD_CLOEXEC = 0o2000000 + + fun eventfd(count : UInt, flags : Int) : Int +end diff --git a/src/lib_c/aarch64-android/c/sys/resource.cr b/src/lib_c/aarch64-android/c/sys/resource.cr index c6bfe1cf2e7b..52fe82cd446a 100644 --- a/src/lib_c/aarch64-android/c/sys/resource.cr +++ b/src/lib_c/aarch64-android/c/sys/resource.cr @@ -22,4 +22,15 @@ lib LibC RUSAGE_CHILDREN = -1 fun getrusage(__who : Int, __usage : RUsage*) : Int + + alias RlimT = ULongLong + + struct Rlimit + rlim_cur : RlimT + rlim_max : RlimT + end + + RLIMIT_NOFILE = 7 + + fun getrlimit(resource : Int, rlim : Rlimit*) : Int end diff --git a/src/lib_c/aarch64-android/c/sys/timerfd.cr b/src/lib_c/aarch64-android/c/sys/timerfd.cr new file mode 100644 index 000000000000..0632646b0e14 --- /dev/null +++ b/src/lib_c/aarch64-android/c/sys/timerfd.cr @@ -0,0 +1,10 @@ +require "../time" + +lib LibC + TFD_NONBLOCK = 0o0004000 + TFD_CLOEXEC = 0o2000000 + TFD_TIMER_ABSTIME = 1 << 0 + + fun timerfd_create(ClockidT, Int) : Int + fun timerfd_settime(Int, Int, Itimerspec*, Itimerspec*) : Int +end diff --git a/src/lib_c/aarch64-android/c/time.cr b/src/lib_c/aarch64-android/c/time.cr index 8f8b81291f0d..5007584d3069 100644 --- a/src/lib_c/aarch64-android/c/time.cr +++ b/src/lib_c/aarch64-android/c/time.cr @@ -23,6 +23,11 @@ lib LibC tv_nsec : Long end + struct Itimerspec + it_interval : Timespec + it_value : Timespec + end + fun clock_gettime(__clock : ClockidT, __ts : Timespec*) : Int fun clock_settime(__clock : ClockidT, __ts : Timespec*) : Int fun gmtime_r(__t : TimeT*, __tm : Tm*) : Tm* diff --git a/src/lib_c/aarch64-darwin/c/sys/event.cr b/src/lib_c/aarch64-darwin/c/sys/event.cr new file mode 100644 index 000000000000..1fd68b6d1975 --- /dev/null +++ b/src/lib_c/aarch64-darwin/c/sys/event.cr @@ -0,0 +1,31 @@ +require "../time" + +lib LibC + EVFILT_READ = -1_i16 + EVFILT_WRITE = -2_i16 + EVFILT_TIMER = -7_i16 + EVFILT_USER = -10_i16 + + EV_ADD = 0x0001_u16 + EV_DELETE = 0x0002_u16 + EV_ENABLE = 0x0004_u16 + EV_ONESHOT = 0x0010_u16 + EV_CLEAR = 0x0020_u16 + EV_EOF = 0x8000_u16 + EV_ERROR = 0x4000_u16 + + NOTE_NSECONDS = 0x00000004_u32 + NOTE_TRIGGER = 0x01000000_u32 + + struct Kevent + ident : SizeT # UintptrT + filter : Int16 + flags : UInt16 + fflags : UInt32 + data : SSizeT # IntptrT + udata : Void* + end + + fun kqueue : Int + fun kevent(kq : Int, changelist : Kevent*, nchanges : Int, eventlist : Kevent*, nevents : Int, timeout : Timespec*) : Int +end diff --git a/src/lib_c/aarch64-darwin/c/sys/resource.cr b/src/lib_c/aarch64-darwin/c/sys/resource.cr index daa583ac5895..4759e8c9b3e3 100644 --- a/src/lib_c/aarch64-darwin/c/sys/resource.cr +++ b/src/lib_c/aarch64-darwin/c/sys/resource.cr @@ -6,6 +6,8 @@ lib LibC rlim_max : RlimT end + RLIMIT_NOFILE = 8 + fun getrlimit(Int, Rlimit*) : Int RLIMIT_STACK = 3 diff --git a/src/lib_c/aarch64-linux-gnu/c/sys/epoll.cr b/src/lib_c/aarch64-linux-gnu/c/sys/epoll.cr new file mode 100644 index 000000000000..ba34b8414732 --- /dev/null +++ b/src/lib_c/aarch64-linux-gnu/c/sys/epoll.cr @@ -0,0 +1,32 @@ +lib LibC + EPOLLIN = 0x001_u32 + EPOLLOUT = 0x004_u32 + EPOLLERR = 0x008_u32 + EPOLLHUP = 0x010_u32 + EPOLLRDHUP = 0x2000_u32 + + EPOLLEXCLUSIVE = 1_u32 << 28 + EPOLLET = 1_u32 << 31 + + EPOLL_CTL_ADD = 1 + EPOLL_CTL_DEL = 2 + EPOLL_CTL_MOD = 3 + + EPOLL_CLOEXEC = 0o2000000 + + union EpollDataT + ptr : Void* + fd : Int + u32 : UInt32 + u64 : UInt64 + end + + struct EpollEvent + events : UInt32 + data : EpollDataT + end + + fun epoll_create1(Int) : Int + fun epoll_ctl(Int, Int, Int, EpollEvent*) : Int + fun epoll_wait(Int, EpollEvent*, Int, Int) : Int +end diff --git a/src/lib_c/aarch64-linux-gnu/c/sys/eventfd.cr b/src/lib_c/aarch64-linux-gnu/c/sys/eventfd.cr new file mode 100644 index 000000000000..12f24428a8f4 --- /dev/null +++ b/src/lib_c/aarch64-linux-gnu/c/sys/eventfd.cr @@ -0,0 +1,5 @@ +lib LibC + EFD_CLOEXEC = 0o2000000 + + fun eventfd(count : UInt, flags : Int) : Int +end diff --git a/src/lib_c/aarch64-linux-gnu/c/sys/resource.cr b/src/lib_c/aarch64-linux-gnu/c/sys/resource.cr index a0900a4730c4..444c4ba692c8 100644 --- a/src/lib_c/aarch64-linux-gnu/c/sys/resource.cr +++ b/src/lib_c/aarch64-linux-gnu/c/sys/resource.cr @@ -22,4 +22,15 @@ lib LibC RUSAGE_CHILDREN = -1 fun getrusage(who : Int, usage : RUsage*) : Int + + alias RlimT = ULongLong + + struct Rlimit + rlim_cur : RlimT + rlim_max : RlimT + end + + RLIMIT_NOFILE = 7 + + fun getrlimit(resource : Int, rlim : Rlimit*) : Int end diff --git a/src/lib_c/aarch64-linux-gnu/c/sys/timerfd.cr b/src/lib_c/aarch64-linux-gnu/c/sys/timerfd.cr new file mode 100644 index 000000000000..0632646b0e14 --- /dev/null +++ b/src/lib_c/aarch64-linux-gnu/c/sys/timerfd.cr @@ -0,0 +1,10 @@ +require "../time" + +lib LibC + TFD_NONBLOCK = 0o0004000 + TFD_CLOEXEC = 0o2000000 + TFD_TIMER_ABSTIME = 1 << 0 + + fun timerfd_create(ClockidT, Int) : Int + fun timerfd_settime(Int, Int, Itimerspec*, Itimerspec*) : Int +end diff --git a/src/lib_c/aarch64-linux-gnu/c/time.cr b/src/lib_c/aarch64-linux-gnu/c/time.cr index 710d477e269b..d00579281b41 100644 --- a/src/lib_c/aarch64-linux-gnu/c/time.cr +++ b/src/lib_c/aarch64-linux-gnu/c/time.cr @@ -23,6 +23,11 @@ lib LibC tv_nsec : Long end + struct Itimerspec + it_interval : Timespec + it_value : Timespec + end + fun clock_gettime(clock_id : ClockidT, tp : Timespec*) : Int fun clock_settime(clock_id : ClockidT, tp : Timespec*) : Int fun gmtime_r(timer : TimeT*, tp : Tm*) : Tm* diff --git a/src/lib_c/aarch64-linux-musl/c/sys/epoll.cr b/src/lib_c/aarch64-linux-musl/c/sys/epoll.cr new file mode 100644 index 000000000000..ba34b8414732 --- /dev/null +++ b/src/lib_c/aarch64-linux-musl/c/sys/epoll.cr @@ -0,0 +1,32 @@ +lib LibC + EPOLLIN = 0x001_u32 + EPOLLOUT = 0x004_u32 + EPOLLERR = 0x008_u32 + EPOLLHUP = 0x010_u32 + EPOLLRDHUP = 0x2000_u32 + + EPOLLEXCLUSIVE = 1_u32 << 28 + EPOLLET = 1_u32 << 31 + + EPOLL_CTL_ADD = 1 + EPOLL_CTL_DEL = 2 + EPOLL_CTL_MOD = 3 + + EPOLL_CLOEXEC = 0o2000000 + + union EpollDataT + ptr : Void* + fd : Int + u32 : UInt32 + u64 : UInt64 + end + + struct EpollEvent + events : UInt32 + data : EpollDataT + end + + fun epoll_create1(Int) : Int + fun epoll_ctl(Int, Int, Int, EpollEvent*) : Int + fun epoll_wait(Int, EpollEvent*, Int, Int) : Int +end diff --git a/src/lib_c/aarch64-linux-musl/c/sys/eventfd.cr b/src/lib_c/aarch64-linux-musl/c/sys/eventfd.cr new file mode 100644 index 000000000000..12f24428a8f4 --- /dev/null +++ b/src/lib_c/aarch64-linux-musl/c/sys/eventfd.cr @@ -0,0 +1,5 @@ +lib LibC + EFD_CLOEXEC = 0o2000000 + + fun eventfd(count : UInt, flags : Int) : Int +end diff --git a/src/lib_c/aarch64-linux-musl/c/sys/resource.cr b/src/lib_c/aarch64-linux-musl/c/sys/resource.cr index daa583ac5895..656e43cb0379 100644 --- a/src/lib_c/aarch64-linux-musl/c/sys/resource.cr +++ b/src/lib_c/aarch64-linux-musl/c/sys/resource.cr @@ -6,6 +6,8 @@ lib LibC rlim_max : RlimT end + RLIMIT_NOFILE = 7 + fun getrlimit(Int, Rlimit*) : Int RLIMIT_STACK = 3 diff --git a/src/lib_c/aarch64-linux-musl/c/sys/timerfd.cr b/src/lib_c/aarch64-linux-musl/c/sys/timerfd.cr new file mode 100644 index 000000000000..0632646b0e14 --- /dev/null +++ b/src/lib_c/aarch64-linux-musl/c/sys/timerfd.cr @@ -0,0 +1,10 @@ +require "../time" + +lib LibC + TFD_NONBLOCK = 0o0004000 + TFD_CLOEXEC = 0o2000000 + TFD_TIMER_ABSTIME = 1 << 0 + + fun timerfd_create(ClockidT, Int) : Int + fun timerfd_settime(Int, Int, Itimerspec*, Itimerspec*) : Int +end diff --git a/src/lib_c/aarch64-linux-musl/c/time.cr b/src/lib_c/aarch64-linux-musl/c/time.cr index f687c8b35db4..4bf25a7f9efc 100644 --- a/src/lib_c/aarch64-linux-musl/c/time.cr +++ b/src/lib_c/aarch64-linux-musl/c/time.cr @@ -23,6 +23,11 @@ lib LibC tv_nsec : Long end + struct Itimerspec + it_interval : Timespec + it_value : Timespec + end + fun clock_gettime(x0 : ClockidT, x1 : Timespec*) : Int fun clock_settime(x0 : ClockidT, x1 : Timespec*) : Int fun gmtime_r(x0 : TimeT*, x1 : Tm*) : Tm* diff --git a/src/lib_c/aarch64-windows-gnu b/src/lib_c/aarch64-windows-gnu new file mode 120000 index 000000000000..072348f65d09 --- /dev/null +++ b/src/lib_c/aarch64-windows-gnu @@ -0,0 +1 @@ +x86_64-windows-msvc \ No newline at end of file diff --git a/src/lib_c/arm-linux-gnueabihf/c/sys/epoll.cr b/src/lib_c/arm-linux-gnueabihf/c/sys/epoll.cr new file mode 100644 index 000000000000..ba34b8414732 --- /dev/null +++ b/src/lib_c/arm-linux-gnueabihf/c/sys/epoll.cr @@ -0,0 +1,32 @@ +lib LibC + EPOLLIN = 0x001_u32 + EPOLLOUT = 0x004_u32 + EPOLLERR = 0x008_u32 + EPOLLHUP = 0x010_u32 + EPOLLRDHUP = 0x2000_u32 + + EPOLLEXCLUSIVE = 1_u32 << 28 + EPOLLET = 1_u32 << 31 + + EPOLL_CTL_ADD = 1 + EPOLL_CTL_DEL = 2 + EPOLL_CTL_MOD = 3 + + EPOLL_CLOEXEC = 0o2000000 + + union EpollDataT + ptr : Void* + fd : Int + u32 : UInt32 + u64 : UInt64 + end + + struct EpollEvent + events : UInt32 + data : EpollDataT + end + + fun epoll_create1(Int) : Int + fun epoll_ctl(Int, Int, Int, EpollEvent*) : Int + fun epoll_wait(Int, EpollEvent*, Int, Int) : Int +end diff --git a/src/lib_c/arm-linux-gnueabihf/c/sys/eventfd.cr b/src/lib_c/arm-linux-gnueabihf/c/sys/eventfd.cr new file mode 100644 index 000000000000..12f24428a8f4 --- /dev/null +++ b/src/lib_c/arm-linux-gnueabihf/c/sys/eventfd.cr @@ -0,0 +1,5 @@ +lib LibC + EFD_CLOEXEC = 0o2000000 + + fun eventfd(count : UInt, flags : Int) : Int +end diff --git a/src/lib_c/arm-linux-gnueabihf/c/sys/resource.cr b/src/lib_c/arm-linux-gnueabihf/c/sys/resource.cr index 7f550c37a622..1c2c2fb678f5 100644 --- a/src/lib_c/arm-linux-gnueabihf/c/sys/resource.cr +++ b/src/lib_c/arm-linux-gnueabihf/c/sys/resource.cr @@ -22,4 +22,15 @@ lib LibC RUSAGE_CHILDREN = -1 fun getrusage(who : Int, usage : RUsage*) : Int16 + + alias RlimT = ULongLong + + struct Rlimit + rlim_cur : RlimT + rlim_max : RlimT + end + + RLIMIT_NOFILE = 7 + + fun getrlimit(resource : Int, rlim : Rlimit*) : Int end diff --git a/src/lib_c/arm-linux-gnueabihf/c/sys/timerfd.cr b/src/lib_c/arm-linux-gnueabihf/c/sys/timerfd.cr new file mode 100644 index 000000000000..0632646b0e14 --- /dev/null +++ b/src/lib_c/arm-linux-gnueabihf/c/sys/timerfd.cr @@ -0,0 +1,10 @@ +require "../time" + +lib LibC + TFD_NONBLOCK = 0o0004000 + TFD_CLOEXEC = 0o2000000 + TFD_TIMER_ABSTIME = 1 << 0 + + fun timerfd_create(ClockidT, Int) : Int + fun timerfd_settime(Int, Int, Itimerspec*, Itimerspec*) : Int +end diff --git a/src/lib_c/arm-linux-gnueabihf/c/time.cr b/src/lib_c/arm-linux-gnueabihf/c/time.cr index 710d477e269b..d00579281b41 100644 --- a/src/lib_c/arm-linux-gnueabihf/c/time.cr +++ b/src/lib_c/arm-linux-gnueabihf/c/time.cr @@ -23,6 +23,11 @@ lib LibC tv_nsec : Long end + struct Itimerspec + it_interval : Timespec + it_value : Timespec + end + fun clock_gettime(clock_id : ClockidT, tp : Timespec*) : Int fun clock_settime(clock_id : ClockidT, tp : Timespec*) : Int fun gmtime_r(timer : TimeT*, tp : Tm*) : Tm* diff --git a/src/lib_c/i386-linux-gnu/c/sys/epoll.cr b/src/lib_c/i386-linux-gnu/c/sys/epoll.cr new file mode 100644 index 000000000000..ba34b8414732 --- /dev/null +++ b/src/lib_c/i386-linux-gnu/c/sys/epoll.cr @@ -0,0 +1,32 @@ +lib LibC + EPOLLIN = 0x001_u32 + EPOLLOUT = 0x004_u32 + EPOLLERR = 0x008_u32 + EPOLLHUP = 0x010_u32 + EPOLLRDHUP = 0x2000_u32 + + EPOLLEXCLUSIVE = 1_u32 << 28 + EPOLLET = 1_u32 << 31 + + EPOLL_CTL_ADD = 1 + EPOLL_CTL_DEL = 2 + EPOLL_CTL_MOD = 3 + + EPOLL_CLOEXEC = 0o2000000 + + union EpollDataT + ptr : Void* + fd : Int + u32 : UInt32 + u64 : UInt64 + end + + struct EpollEvent + events : UInt32 + data : EpollDataT + end + + fun epoll_create1(Int) : Int + fun epoll_ctl(Int, Int, Int, EpollEvent*) : Int + fun epoll_wait(Int, EpollEvent*, Int, Int) : Int +end diff --git a/src/lib_c/i386-linux-gnu/c/sys/eventfd.cr b/src/lib_c/i386-linux-gnu/c/sys/eventfd.cr new file mode 100644 index 000000000000..12f24428a8f4 --- /dev/null +++ b/src/lib_c/i386-linux-gnu/c/sys/eventfd.cr @@ -0,0 +1,5 @@ +lib LibC + EFD_CLOEXEC = 0o2000000 + + fun eventfd(count : UInt, flags : Int) : Int +end diff --git a/src/lib_c/i386-linux-gnu/c/sys/resource.cr b/src/lib_c/i386-linux-gnu/c/sys/resource.cr index a0900a4730c4..444c4ba692c8 100644 --- a/src/lib_c/i386-linux-gnu/c/sys/resource.cr +++ b/src/lib_c/i386-linux-gnu/c/sys/resource.cr @@ -22,4 +22,15 @@ lib LibC RUSAGE_CHILDREN = -1 fun getrusage(who : Int, usage : RUsage*) : Int + + alias RlimT = ULongLong + + struct Rlimit + rlim_cur : RlimT + rlim_max : RlimT + end + + RLIMIT_NOFILE = 7 + + fun getrlimit(resource : Int, rlim : Rlimit*) : Int end diff --git a/src/lib_c/i386-linux-gnu/c/sys/timerfd.cr b/src/lib_c/i386-linux-gnu/c/sys/timerfd.cr new file mode 100644 index 000000000000..0632646b0e14 --- /dev/null +++ b/src/lib_c/i386-linux-gnu/c/sys/timerfd.cr @@ -0,0 +1,10 @@ +require "../time" + +lib LibC + TFD_NONBLOCK = 0o0004000 + TFD_CLOEXEC = 0o2000000 + TFD_TIMER_ABSTIME = 1 << 0 + + fun timerfd_create(ClockidT, Int) : Int + fun timerfd_settime(Int, Int, Itimerspec*, Itimerspec*) : Int +end diff --git a/src/lib_c/i386-linux-gnu/c/time.cr b/src/lib_c/i386-linux-gnu/c/time.cr index 710d477e269b..d00579281b41 100644 --- a/src/lib_c/i386-linux-gnu/c/time.cr +++ b/src/lib_c/i386-linux-gnu/c/time.cr @@ -23,6 +23,11 @@ lib LibC tv_nsec : Long end + struct Itimerspec + it_interval : Timespec + it_value : Timespec + end + fun clock_gettime(clock_id : ClockidT, tp : Timespec*) : Int fun clock_settime(clock_id : ClockidT, tp : Timespec*) : Int fun gmtime_r(timer : TimeT*, tp : Tm*) : Tm* diff --git a/src/lib_c/i386-linux-musl/c/sys/epoll.cr b/src/lib_c/i386-linux-musl/c/sys/epoll.cr new file mode 100644 index 000000000000..ba34b8414732 --- /dev/null +++ b/src/lib_c/i386-linux-musl/c/sys/epoll.cr @@ -0,0 +1,32 @@ +lib LibC + EPOLLIN = 0x001_u32 + EPOLLOUT = 0x004_u32 + EPOLLERR = 0x008_u32 + EPOLLHUP = 0x010_u32 + EPOLLRDHUP = 0x2000_u32 + + EPOLLEXCLUSIVE = 1_u32 << 28 + EPOLLET = 1_u32 << 31 + + EPOLL_CTL_ADD = 1 + EPOLL_CTL_DEL = 2 + EPOLL_CTL_MOD = 3 + + EPOLL_CLOEXEC = 0o2000000 + + union EpollDataT + ptr : Void* + fd : Int + u32 : UInt32 + u64 : UInt64 + end + + struct EpollEvent + events : UInt32 + data : EpollDataT + end + + fun epoll_create1(Int) : Int + fun epoll_ctl(Int, Int, Int, EpollEvent*) : Int + fun epoll_wait(Int, EpollEvent*, Int, Int) : Int +end diff --git a/src/lib_c/i386-linux-musl/c/sys/eventfd.cr b/src/lib_c/i386-linux-musl/c/sys/eventfd.cr new file mode 100644 index 000000000000..12f24428a8f4 --- /dev/null +++ b/src/lib_c/i386-linux-musl/c/sys/eventfd.cr @@ -0,0 +1,5 @@ +lib LibC + EFD_CLOEXEC = 0o2000000 + + fun eventfd(count : UInt, flags : Int) : Int +end diff --git a/src/lib_c/i386-linux-musl/c/sys/resource.cr b/src/lib_c/i386-linux-musl/c/sys/resource.cr index daa583ac5895..656e43cb0379 100644 --- a/src/lib_c/i386-linux-musl/c/sys/resource.cr +++ b/src/lib_c/i386-linux-musl/c/sys/resource.cr @@ -6,6 +6,8 @@ lib LibC rlim_max : RlimT end + RLIMIT_NOFILE = 7 + fun getrlimit(Int, Rlimit*) : Int RLIMIT_STACK = 3 diff --git a/src/lib_c/i386-linux-musl/c/sys/timerfd.cr b/src/lib_c/i386-linux-musl/c/sys/timerfd.cr new file mode 100644 index 000000000000..0632646b0e14 --- /dev/null +++ b/src/lib_c/i386-linux-musl/c/sys/timerfd.cr @@ -0,0 +1,10 @@ +require "../time" + +lib LibC + TFD_NONBLOCK = 0o0004000 + TFD_CLOEXEC = 0o2000000 + TFD_TIMER_ABSTIME = 1 << 0 + + fun timerfd_create(ClockidT, Int) : Int + fun timerfd_settime(Int, Int, Itimerspec*, Itimerspec*) : Int +end diff --git a/src/lib_c/i386-linux-musl/c/time.cr b/src/lib_c/i386-linux-musl/c/time.cr index f687c8b35db4..4bf25a7f9efc 100644 --- a/src/lib_c/i386-linux-musl/c/time.cr +++ b/src/lib_c/i386-linux-musl/c/time.cr @@ -23,6 +23,11 @@ lib LibC tv_nsec : Long end + struct Itimerspec + it_interval : Timespec + it_value : Timespec + end + fun clock_gettime(x0 : ClockidT, x1 : Timespec*) : Int fun clock_settime(x0 : ClockidT, x1 : Timespec*) : Int fun gmtime_r(x0 : TimeT*, x1 : Tm*) : Tm* diff --git a/src/lib_c/x86_64-darwin/c/sys/event.cr b/src/lib_c/x86_64-darwin/c/sys/event.cr new file mode 100644 index 000000000000..1fd68b6d1975 --- /dev/null +++ b/src/lib_c/x86_64-darwin/c/sys/event.cr @@ -0,0 +1,31 @@ +require "../time" + +lib LibC + EVFILT_READ = -1_i16 + EVFILT_WRITE = -2_i16 + EVFILT_TIMER = -7_i16 + EVFILT_USER = -10_i16 + + EV_ADD = 0x0001_u16 + EV_DELETE = 0x0002_u16 + EV_ENABLE = 0x0004_u16 + EV_ONESHOT = 0x0010_u16 + EV_CLEAR = 0x0020_u16 + EV_EOF = 0x8000_u16 + EV_ERROR = 0x4000_u16 + + NOTE_NSECONDS = 0x00000004_u32 + NOTE_TRIGGER = 0x01000000_u32 + + struct Kevent + ident : SizeT # UintptrT + filter : Int16 + flags : UInt16 + fflags : UInt32 + data : SSizeT # IntptrT + udata : Void* + end + + fun kqueue : Int + fun kevent(kq : Int, changelist : Kevent*, nchanges : Int, eventlist : Kevent*, nevents : Int, timeout : Timespec*) : Int +end diff --git a/src/lib_c/x86_64-darwin/c/sys/resource.cr b/src/lib_c/x86_64-darwin/c/sys/resource.cr index daa583ac5895..4759e8c9b3e3 100644 --- a/src/lib_c/x86_64-darwin/c/sys/resource.cr +++ b/src/lib_c/x86_64-darwin/c/sys/resource.cr @@ -6,6 +6,8 @@ lib LibC rlim_max : RlimT end + RLIMIT_NOFILE = 8 + fun getrlimit(Int, Rlimit*) : Int RLIMIT_STACK = 3 diff --git a/src/lib_c/x86_64-dragonfly/c/sys/event.cr b/src/lib_c/x86_64-dragonfly/c/sys/event.cr new file mode 100644 index 000000000000..aff6274b8fd1 --- /dev/null +++ b/src/lib_c/x86_64-dragonfly/c/sys/event.cr @@ -0,0 +1,30 @@ +require "../time" + +lib LibC + EVFILT_READ = -1_i16 + EVFILT_WRITE = -2_i16 + EVFILT_TIMER = -7_i16 + EVFILT_USER = -9_i16 + + EV_ADD = 0x0001_u16 + EV_DELETE = 0x0002_u16 + EV_ENABLE = 0x0004_u16 + EV_ONESHOT = 0x0010_u16 + EV_CLEAR = 0x0020_u16 + EV_EOF = 0x8000_u16 + EV_ERROR = 0x4000_u16 + + NOTE_TRIGGER = 0x01000000_u32 + + struct Kevent + ident : SizeT # UintptrT + filter : Short + flags : UShort + fflags : UInt + data : SSizeT # IntptrT + udata : Void* + end + + fun kqueue : Int + fun kevent(kq : Int, changelist : Kevent*, nchanges : Int, eventlist : Kevent*, nevents : Int, timeout : Timespec*) : Int +end diff --git a/src/lib_c/x86_64-dragonfly/c/sys/resource.cr b/src/lib_c/x86_64-dragonfly/c/sys/resource.cr index d52182f69bce..388b52651f21 100644 --- a/src/lib_c/x86_64-dragonfly/c/sys/resource.cr +++ b/src/lib_c/x86_64-dragonfly/c/sys/resource.cr @@ -22,4 +22,15 @@ lib LibC RUSAGE_CHILDREN = -1 fun getrusage(who : Int, usage : RUsage*) : Int + + alias RlimT = UInt64 + + struct Rlimit + rlim_cur : RlimT + rlim_max : RlimT + end + + RLIMIT_NOFILE = 8 + + fun getrlimit(resource : Int, rlim : Rlimit*) : Int end diff --git a/src/lib_c/x86_64-freebsd/c/sys/event.cr b/src/lib_c/x86_64-freebsd/c/sys/event.cr new file mode 100644 index 000000000000..0abe0686aba0 --- /dev/null +++ b/src/lib_c/x86_64-freebsd/c/sys/event.cr @@ -0,0 +1,32 @@ +require "../time" + +lib LibC + EVFILT_READ = -1_i16 + EVFILT_WRITE = -2_i16 + EVFILT_TIMER = -7_i16 + EVFILT_USER = -11_i16 + + EV_ADD = 0x0001_u16 + EV_DELETE = 0x0002_u16 + EV_ENABLE = 0x0004_u16 + EV_ONESHOT = 0x0010_u16 + EV_CLEAR = 0x0020_u16 + EV_EOF = 0x8000_u16 + EV_ERROR = 0x4000_u16 + + NOTE_NSECONDS = 0x00000008_u32 + NOTE_TRIGGER = 0x01000000_u32 + + struct Kevent + ident : SizeT # UintptrT + filter : Short + flags : UShort + fflags : UInt + data : Int64 + udata : Void* + ext : UInt64[4] + end + + fun kqueue1(flags : Int) : Int + fun kevent(kq : Int, changelist : Kevent*, nchanges : Int, eventlist : Kevent*, nevents : Int, timeout : Timespec*) : Int +end diff --git a/src/lib_c/x86_64-freebsd/c/sys/resource.cr b/src/lib_c/x86_64-freebsd/c/sys/resource.cr index 7f550c37a622..6f078dda986d 100644 --- a/src/lib_c/x86_64-freebsd/c/sys/resource.cr +++ b/src/lib_c/x86_64-freebsd/c/sys/resource.cr @@ -22,4 +22,15 @@ lib LibC RUSAGE_CHILDREN = -1 fun getrusage(who : Int, usage : RUsage*) : Int16 + + alias RlimT = UInt64 + + struct Rlimit + rlim_cur : RlimT + rlim_max : RlimT + end + + RLIMIT_NOFILE = 8 + + fun getrlimit(resource : Int, rlim : Rlimit*) : Int end diff --git a/src/lib_c/x86_64-linux-gnu/c/sys/epoll.cr b/src/lib_c/x86_64-linux-gnu/c/sys/epoll.cr new file mode 100644 index 000000000000..4dc752f64652 --- /dev/null +++ b/src/lib_c/x86_64-linux-gnu/c/sys/epoll.cr @@ -0,0 +1,33 @@ +lib LibC + EPOLLIN = 0x001_u32 + EPOLLOUT = 0x004_u32 + EPOLLERR = 0x008_u32 + EPOLLHUP = 0x010_u32 + EPOLLRDHUP = 0x2000_u32 + + EPOLLEXCLUSIVE = 1_u32 << 28 + EPOLLET = 1_u32 << 31 + + EPOLL_CTL_ADD = 1 + EPOLL_CTL_DEL = 2 + EPOLL_CTL_MOD = 3 + + EPOLL_CLOEXEC = 0o2000000 + + union EpollDataT + ptr : Void* + fd : Int + u32 : UInt32 + u64 : UInt64 + end + + @[Packed] + struct EpollEvent + events : UInt32 + data : EpollDataT + end + + fun epoll_create1(Int) : Int + fun epoll_ctl(Int, Int, Int, EpollEvent*) : Int + fun epoll_wait(Int, EpollEvent*, Int, Int) : Int +end diff --git a/src/lib_c/x86_64-linux-gnu/c/sys/eventfd.cr b/src/lib_c/x86_64-linux-gnu/c/sys/eventfd.cr new file mode 100644 index 000000000000..12f24428a8f4 --- /dev/null +++ b/src/lib_c/x86_64-linux-gnu/c/sys/eventfd.cr @@ -0,0 +1,5 @@ +lib LibC + EFD_CLOEXEC = 0o2000000 + + fun eventfd(count : UInt, flags : Int) : Int +end diff --git a/src/lib_c/x86_64-linux-gnu/c/sys/resource.cr b/src/lib_c/x86_64-linux-gnu/c/sys/resource.cr index a0900a4730c4..444c4ba692c8 100644 --- a/src/lib_c/x86_64-linux-gnu/c/sys/resource.cr +++ b/src/lib_c/x86_64-linux-gnu/c/sys/resource.cr @@ -22,4 +22,15 @@ lib LibC RUSAGE_CHILDREN = -1 fun getrusage(who : Int, usage : RUsage*) : Int + + alias RlimT = ULongLong + + struct Rlimit + rlim_cur : RlimT + rlim_max : RlimT + end + + RLIMIT_NOFILE = 7 + + fun getrlimit(resource : Int, rlim : Rlimit*) : Int end diff --git a/src/lib_c/x86_64-linux-gnu/c/sys/timerfd.cr b/src/lib_c/x86_64-linux-gnu/c/sys/timerfd.cr new file mode 100644 index 000000000000..0632646b0e14 --- /dev/null +++ b/src/lib_c/x86_64-linux-gnu/c/sys/timerfd.cr @@ -0,0 +1,10 @@ +require "../time" + +lib LibC + TFD_NONBLOCK = 0o0004000 + TFD_CLOEXEC = 0o2000000 + TFD_TIMER_ABSTIME = 1 << 0 + + fun timerfd_create(ClockidT, Int) : Int + fun timerfd_settime(Int, Int, Itimerspec*, Itimerspec*) : Int +end diff --git a/src/lib_c/x86_64-linux-gnu/c/time.cr b/src/lib_c/x86_64-linux-gnu/c/time.cr index 710d477e269b..d00579281b41 100644 --- a/src/lib_c/x86_64-linux-gnu/c/time.cr +++ b/src/lib_c/x86_64-linux-gnu/c/time.cr @@ -23,6 +23,11 @@ lib LibC tv_nsec : Long end + struct Itimerspec + it_interval : Timespec + it_value : Timespec + end + fun clock_gettime(clock_id : ClockidT, tp : Timespec*) : Int fun clock_settime(clock_id : ClockidT, tp : Timespec*) : Int fun gmtime_r(timer : TimeT*, tp : Tm*) : Tm* diff --git a/src/lib_c/x86_64-linux-musl/c/sys/epoll.cr b/src/lib_c/x86_64-linux-musl/c/sys/epoll.cr new file mode 100644 index 000000000000..4dc752f64652 --- /dev/null +++ b/src/lib_c/x86_64-linux-musl/c/sys/epoll.cr @@ -0,0 +1,33 @@ +lib LibC + EPOLLIN = 0x001_u32 + EPOLLOUT = 0x004_u32 + EPOLLERR = 0x008_u32 + EPOLLHUP = 0x010_u32 + EPOLLRDHUP = 0x2000_u32 + + EPOLLEXCLUSIVE = 1_u32 << 28 + EPOLLET = 1_u32 << 31 + + EPOLL_CTL_ADD = 1 + EPOLL_CTL_DEL = 2 + EPOLL_CTL_MOD = 3 + + EPOLL_CLOEXEC = 0o2000000 + + union EpollDataT + ptr : Void* + fd : Int + u32 : UInt32 + u64 : UInt64 + end + + @[Packed] + struct EpollEvent + events : UInt32 + data : EpollDataT + end + + fun epoll_create1(Int) : Int + fun epoll_ctl(Int, Int, Int, EpollEvent*) : Int + fun epoll_wait(Int, EpollEvent*, Int, Int) : Int +end diff --git a/src/lib_c/x86_64-linux-musl/c/sys/eventfd.cr b/src/lib_c/x86_64-linux-musl/c/sys/eventfd.cr new file mode 100644 index 000000000000..12f24428a8f4 --- /dev/null +++ b/src/lib_c/x86_64-linux-musl/c/sys/eventfd.cr @@ -0,0 +1,5 @@ +lib LibC + EFD_CLOEXEC = 0o2000000 + + fun eventfd(count : UInt, flags : Int) : Int +end diff --git a/src/lib_c/x86_64-linux-musl/c/sys/resource.cr b/src/lib_c/x86_64-linux-musl/c/sys/resource.cr index daa583ac5895..656e43cb0379 100644 --- a/src/lib_c/x86_64-linux-musl/c/sys/resource.cr +++ b/src/lib_c/x86_64-linux-musl/c/sys/resource.cr @@ -6,6 +6,8 @@ lib LibC rlim_max : RlimT end + RLIMIT_NOFILE = 7 + fun getrlimit(Int, Rlimit*) : Int RLIMIT_STACK = 3 diff --git a/src/lib_c/x86_64-linux-musl/c/sys/timerfd.cr b/src/lib_c/x86_64-linux-musl/c/sys/timerfd.cr new file mode 100644 index 000000000000..0632646b0e14 --- /dev/null +++ b/src/lib_c/x86_64-linux-musl/c/sys/timerfd.cr @@ -0,0 +1,10 @@ +require "../time" + +lib LibC + TFD_NONBLOCK = 0o0004000 + TFD_CLOEXEC = 0o2000000 + TFD_TIMER_ABSTIME = 1 << 0 + + fun timerfd_create(ClockidT, Int) : Int + fun timerfd_settime(Int, Int, Itimerspec*, Itimerspec*) : Int +end diff --git a/src/lib_c/x86_64-linux-musl/c/time.cr b/src/lib_c/x86_64-linux-musl/c/time.cr index f687c8b35db4..4bf25a7f9efc 100644 --- a/src/lib_c/x86_64-linux-musl/c/time.cr +++ b/src/lib_c/x86_64-linux-musl/c/time.cr @@ -23,6 +23,11 @@ lib LibC tv_nsec : Long end + struct Itimerspec + it_interval : Timespec + it_value : Timespec + end + fun clock_gettime(x0 : ClockidT, x1 : Timespec*) : Int fun clock_settime(x0 : ClockidT, x1 : Timespec*) : Int fun gmtime_r(x0 : TimeT*, x1 : Tm*) : Tm* diff --git a/src/lib_c/x86_64-netbsd/c/sys/event.cr b/src/lib_c/x86_64-netbsd/c/sys/event.cr new file mode 100644 index 000000000000..91da3cea1a04 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sys/event.cr @@ -0,0 +1,32 @@ +require "../time" + +lib LibC + EVFILT_READ = 0_u32 + EVFILT_WRITE = 1_u32 + EVFILT_TIMER = 6_u32 + EVFILT_USER = 8_u32 + + EV_ADD = 0x0001_u32 + EV_DELETE = 0x0002_u32 + EV_ENABLE = 0x0004_u16 + EV_ONESHOT = 0x0010_u32 + EV_CLEAR = 0x0020_u32 + EV_EOF = 0x8000_u32 + EV_ERROR = 0x4000_u32 + + NOTE_NSECONDS = 0x00000003_u32 + NOTE_TRIGGER = 0x01000000_u32 + + struct Kevent + ident : SizeT # UintptrT + filter : UInt32 + flags : UInt32 + fflags : UInt32 + data : Int64 + udata : Void* + ext : UInt64[4] + end + + fun kqueue1(flags : Int) : Int + fun kevent = __kevent50(kq : Int, changelist : Kevent*, nchanges : SizeT, eventlist : Kevent*, nevents : SizeT, timeout : Timespec*) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/sys/resource.cr b/src/lib_c/x86_64-netbsd/c/sys/resource.cr index d52182f69bce..388b52651f21 100644 --- a/src/lib_c/x86_64-netbsd/c/sys/resource.cr +++ b/src/lib_c/x86_64-netbsd/c/sys/resource.cr @@ -22,4 +22,15 @@ lib LibC RUSAGE_CHILDREN = -1 fun getrusage(who : Int, usage : RUsage*) : Int + + alias RlimT = UInt64 + + struct Rlimit + rlim_cur : RlimT + rlim_max : RlimT + end + + RLIMIT_NOFILE = 8 + + fun getrlimit(resource : Int, rlim : Rlimit*) : Int end diff --git a/src/lib_c/x86_64-openbsd/c/sys/event.cr b/src/lib_c/x86_64-openbsd/c/sys/event.cr new file mode 100644 index 000000000000..b95764cb7f54 --- /dev/null +++ b/src/lib_c/x86_64-openbsd/c/sys/event.cr @@ -0,0 +1,28 @@ +require "../time" + +lib LibC + EVFILT_READ = -1_i16 + EVFILT_WRITE = -2_i16 + EVFILT_TIMER = -7_i16 + + EV_ADD = 0x0001_u16 + EV_DELETE = 0x0002_u16 + EV_ONESHOT = 0x0010_u16 + EV_CLEAR = 0x0020_u16 + EV_EOF = 0x8000_u16 + EV_ERROR = 0x4000_u16 + + NOTE_NSECONDS = 0x00000003_u32 + + struct Kevent + ident : SizeT # UintptrT + filter : Short + flags : UShort + fflags : UInt + data : Int64 + udata : Void* + end + + fun kqueue1(flags : Int) : Int + fun kevent(kq : Int, changelist : Kevent*, nchanges : Int, eventlist : Kevent*, nevents : Int, timeout : Timespec*) : Int +end diff --git a/src/lib_c/x86_64-openbsd/c/sys/resource.cr b/src/lib_c/x86_64-openbsd/c/sys/resource.cr index 7f550c37a622..6f078dda986d 100644 --- a/src/lib_c/x86_64-openbsd/c/sys/resource.cr +++ b/src/lib_c/x86_64-openbsd/c/sys/resource.cr @@ -22,4 +22,15 @@ lib LibC RUSAGE_CHILDREN = -1 fun getrusage(who : Int, usage : RUsage*) : Int16 + + alias RlimT = UInt64 + + struct Rlimit + rlim_cur : RlimT + rlim_max : RlimT + end + + RLIMIT_NOFILE = 8 + + fun getrlimit(resource : Int, rlim : Rlimit*) : Int end diff --git a/src/lib_c/x86_64-solaris/c/sys/epoll.cr b/src/lib_c/x86_64-solaris/c/sys/epoll.cr new file mode 100644 index 000000000000..4dc752f64652 --- /dev/null +++ b/src/lib_c/x86_64-solaris/c/sys/epoll.cr @@ -0,0 +1,33 @@ +lib LibC + EPOLLIN = 0x001_u32 + EPOLLOUT = 0x004_u32 + EPOLLERR = 0x008_u32 + EPOLLHUP = 0x010_u32 + EPOLLRDHUP = 0x2000_u32 + + EPOLLEXCLUSIVE = 1_u32 << 28 + EPOLLET = 1_u32 << 31 + + EPOLL_CTL_ADD = 1 + EPOLL_CTL_DEL = 2 + EPOLL_CTL_MOD = 3 + + EPOLL_CLOEXEC = 0o2000000 + + union EpollDataT + ptr : Void* + fd : Int + u32 : UInt32 + u64 : UInt64 + end + + @[Packed] + struct EpollEvent + events : UInt32 + data : EpollDataT + end + + fun epoll_create1(Int) : Int + fun epoll_ctl(Int, Int, Int, EpollEvent*) : Int + fun epoll_wait(Int, EpollEvent*, Int, Int) : Int +end diff --git a/src/lib_c/x86_64-solaris/c/sys/eventfd.cr b/src/lib_c/x86_64-solaris/c/sys/eventfd.cr new file mode 100644 index 000000000000..12f24428a8f4 --- /dev/null +++ b/src/lib_c/x86_64-solaris/c/sys/eventfd.cr @@ -0,0 +1,5 @@ +lib LibC + EFD_CLOEXEC = 0o2000000 + + fun eventfd(count : UInt, flags : Int) : Int +end diff --git a/src/lib_c/x86_64-solaris/c/sys/resource.cr b/src/lib_c/x86_64-solaris/c/sys/resource.cr index d52182f69bce..74f9b56f9971 100644 --- a/src/lib_c/x86_64-solaris/c/sys/resource.cr +++ b/src/lib_c/x86_64-solaris/c/sys/resource.cr @@ -22,4 +22,15 @@ lib LibC RUSAGE_CHILDREN = -1 fun getrusage(who : Int, usage : RUsage*) : Int + + alias RlimT = ULongLong + + struct Rlimit + rlim_cur : RlimT + rlim_max : RlimT + end + + RLIMIT_NOFILE = 5 + + fun getrlimit(resource : Int, rlim : Rlimit*) : Int end diff --git a/src/lib_c/x86_64-solaris/c/sys/timerfd.cr b/src/lib_c/x86_64-solaris/c/sys/timerfd.cr new file mode 100644 index 000000000000..0632646b0e14 --- /dev/null +++ b/src/lib_c/x86_64-solaris/c/sys/timerfd.cr @@ -0,0 +1,10 @@ +require "../time" + +lib LibC + TFD_NONBLOCK = 0o0004000 + TFD_CLOEXEC = 0o2000000 + TFD_TIMER_ABSTIME = 1 << 0 + + fun timerfd_create(ClockidT, Int) : Int + fun timerfd_settime(Int, Int, Itimerspec*, Itimerspec*) : Int +end diff --git a/src/lib_c/x86_64-solaris/c/time.cr b/src/lib_c/x86_64-solaris/c/time.cr index 531f8e373f4b..0aa8f3fce053 100644 --- a/src/lib_c/x86_64-solaris/c/time.cr +++ b/src/lib_c/x86_64-solaris/c/time.cr @@ -21,6 +21,11 @@ lib LibC tv_nsec : Long end + struct Itimerspec + it_interval : Timespec + it_value : Timespec + end + fun clock_gettime(x0 : ClockidT, x1 : Timespec*) : Int fun clock_settime(x0 : ClockidT, x1 : Timespec*) : Int fun gmtime_r(x0 : TimeT*, x1 : Tm*) : Tm* diff --git a/src/lib_c/x86_64-windows-msvc/c/heapapi.cr b/src/lib_c/x86_64-windows-msvc/c/heapapi.cr index 1738cf774cac..8db5152585bc 100644 --- a/src/lib_c/x86_64-windows-msvc/c/heapapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/heapapi.cr @@ -1,6 +1,8 @@ require "c/winnt" lib LibC + HEAP_ZERO_MEMORY = 0x00000008 + fun GetProcessHeap : HANDLE fun HeapAlloc(hHeap : HANDLE, dwFlags : DWORD, dwBytes : SizeT) : Void* fun HeapReAlloc(hHeap : HANDLE, dwFlags : DWORD, lpMem : Void*, dwBytes : SizeT) : Void* diff --git a/src/lib_z/lib_z.cr b/src/lib_z/lib_z.cr index 1c88cb67bba8..47de2981e2f6 100644 --- a/src/lib_z/lib_z.cr +++ b/src/lib_z/lib_z.cr @@ -1,3 +1,8 @@ +# Supported library versions: +# +# * zlib +# +# See https://crystal-lang.org/reference/man/required_libraries.html#other-stdlib-libraries @[Link("z")] {% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %} @[Link(dll: "zlib1.dll")] diff --git a/src/llvm/lib_llvm.cr b/src/llvm/lib_llvm.cr index 8b6856631b55..1349d5bf6a91 100644 --- a/src/llvm/lib_llvm.cr +++ b/src/llvm/lib_llvm.cr @@ -40,6 +40,11 @@ end {% end %} +# Supported library versions: +# +# * LLVM (8-19; aarch64 requires 13+) +# +# See https://crystal-lang.org/reference/man/required_libraries.html#other-stdlib-libraries {% begin %} lib LibLLVM IS_180 = {{LibLLVM::VERSION.starts_with?("18.0")}} diff --git a/src/openssl/lib_crypto.cr b/src/openssl/lib_crypto.cr index fecc69ad44fc..8e24bbcbc78e 100644 --- a/src/openssl/lib_crypto.cr +++ b/src/openssl/lib_crypto.cr @@ -1,3 +1,9 @@ +# Supported library versions: +# +# * openssl (1.1.0–3.3+) +# * libressl (2.0–4.0+) +# +# See https://crystal-lang.org/reference/man/required_libraries.html#tls {% begin %} lib LibCrypto {% if flag?(:msvc) %} @@ -103,7 +109,7 @@ lib LibCrypto alias BioMethodDestroy = Bio* -> Int alias BioMethodCallbackCtrl = (Bio*, Int, Void*) -> Long - {% if compare_versions(LibCrypto::OPENSSL_VERSION, "1.1.0") >= 0 %} + {% if compare_versions(LibCrypto::OPENSSL_VERSION, "1.1.0") >= 0 || compare_versions(LibCrypto::LIBRESSL_VERSION, "2.7.0") >= 0 %} type BioMethod = Void {% else %} struct BioMethod @@ -123,7 +129,7 @@ lib LibCrypto fun BIO_new(BioMethod*) : Bio* fun BIO_free(Bio*) : Int - {% if compare_versions(LibCrypto::OPENSSL_VERSION, "1.1.0") >= 0 %} + {% if compare_versions(LibCrypto::OPENSSL_VERSION, "1.1.0") >= 0 || compare_versions(LibCrypto::LIBRESSL_VERSION, "2.7.0") >= 0 %} fun BIO_set_data(Bio*, Void*) fun BIO_get_data(Bio*) : Void* fun BIO_set_init(Bio*, Int) @@ -139,6 +145,7 @@ lib LibCrypto fun BIO_meth_set_destroy(BioMethod*, BioMethodDestroy) fun BIO_meth_set_callback_ctrl(BioMethod*, BioMethodCallbackCtrl) {% end %} + # LibreSSL does not define these symbols {% if compare_versions(LibCrypto::OPENSSL_VERSION, "1.1.1") >= 0 %} fun BIO_meth_set_read_ex(BioMethod*, BioMethodRead) fun BIO_meth_set_write_ex(BioMethod*, BioMethodWrite) @@ -223,7 +230,7 @@ lib LibCrypto fun evp_digestfinal_ex = EVP_DigestFinal_ex(ctx : EVP_MD_CTX, md : UInt8*, size : UInt32*) : Int32 - {% if compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 %} + {% if compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 || compare_versions(LibCrypto::LIBRESSL_VERSION, "2.7.0") >= 0 %} fun evp_md_ctx_new = EVP_MD_CTX_new : EVP_MD_CTX fun evp_md_ctx_free = EVP_MD_CTX_free(ctx : EVP_MD_CTX) {% else %} @@ -300,7 +307,7 @@ lib LibCrypto fun md5 = MD5(data : UInt8*, length : LibC::SizeT, md : UInt8*) : UInt8* fun pkcs5_pbkdf2_hmac_sha1 = PKCS5_PBKDF2_HMAC_SHA1(pass : LibC::Char*, passlen : LibC::Int, salt : UInt8*, saltlen : LibC::Int, iter : LibC::Int, keylen : LibC::Int, out : UInt8*) : LibC::Int - {% if compare_versions(OPENSSL_VERSION, "1.0.0") >= 0 %} + {% if compare_versions(OPENSSL_VERSION, "1.0.0") >= 0 || LIBRESSL_VERSION != "0.0.0" %} fun pkcs5_pbkdf2_hmac = PKCS5_PBKDF2_HMAC(pass : LibC::Char*, passlen : LibC::Int, salt : UInt8*, saltlen : LibC::Int, iter : LibC::Int, digest : EVP_MD, keylen : LibC::Int, out : UInt8*) : LibC::Int {% end %} @@ -374,12 +381,12 @@ lib LibCrypto fun x509_store_add_cert = X509_STORE_add_cert(ctx : X509_STORE, x : X509) : Int - {% unless compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 %} + {% unless compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 || compare_versions(LibCrypto::LIBRESSL_VERSION, "3.0.0") >= 0 %} fun err_load_crypto_strings = ERR_load_crypto_strings fun openssl_add_all_algorithms = OPENSSL_add_all_algorithms_noconf {% end %} - {% if compare_versions(OPENSSL_VERSION, "1.0.2") >= 0 %} + {% if compare_versions(OPENSSL_VERSION, "1.0.2") >= 0 || LIBRESSL_VERSION != "0.0.0" %} type X509VerifyParam = Void* @[Flags] diff --git a/src/openssl/lib_ssl.cr b/src/openssl/lib_ssl.cr index 4e7e2def549c..449f35dd0f72 100644 --- a/src/openssl/lib_ssl.cr +++ b/src/openssl/lib_ssl.cr @@ -4,6 +4,12 @@ require "./lib_crypto" {% raise "The `without_openssl` flag is preventing you to use the LibSSL module" %} {% end %} +# Supported library versions: +# +# * openssl (1.1.0–3.3+) +# * libressl (2.0–4.0+) +# +# See https://crystal-lang.org/reference/man/required_libraries.html#tls {% begin %} lib LibSSL {% if flag?(:msvc) %} @@ -139,7 +145,7 @@ lib LibSSL NETSCAPE_DEMO_CIPHER_CHANGE_BUG = 0x40000000 CRYPTOPRO_TLSEXT_BUG = 0x80000000 - {% if compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 %} + {% if compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 || compare_versions(LIBRESSL_VERSION, "2.3.0") >= 0 %} MICROSOFT_SESS_ID_BUG = 0x00000000 NETSCAPE_CHALLENGE_BUG = 0x00000000 NETSCAPE_REUSE_CIPHER_CHANGE_BUG = 0x00000000 @@ -237,6 +243,7 @@ lib LibSSL fun ssl_get_peer_certificate = SSL_get_peer_certificate(handle : SSL) : LibCrypto::X509 {% end %} + # In LibreSSL these functions are implemented as macros {% if compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 %} fun ssl_ctx_get_options = SSL_CTX_get_options(ctx : SSLContext) : ULong fun ssl_ctx_set_options = SSL_CTX_set_options(ctx : SSLContext, larg : ULong) : ULong @@ -251,12 +258,13 @@ lib LibSSL fun ssl_ctx_set_cert_verify_callback = SSL_CTX_set_cert_verify_callback(ctx : SSLContext, callback : CertVerifyCallback, arg : Void*) # control TLS 1.3 session ticket generation + # LibreSSL does not seem to implement these functions {% if compare_versions(OPENSSL_VERSION, "1.1.1") >= 0 %} fun ssl_ctx_set_num_tickets = SSL_CTX_set_num_tickets(ctx : SSLContext, larg : LibC::SizeT) : Int fun ssl_set_num_tickets = SSL_set_num_tickets(ctx : SSL, larg : LibC::SizeT) : Int {% end %} - {% if compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 %} + {% if compare_versions(LibSSL::OPENSSL_VERSION, "1.1.0") >= 0 || compare_versions(LibSSL::LIBRESSL_VERSION, "2.3.0") >= 0 %} fun tls_method = TLS_method : SSLMethod {% else %} fun ssl_library_init = SSL_library_init @@ -264,7 +272,7 @@ lib LibSSL fun sslv23_method = SSLv23_method : SSLMethod {% end %} - {% if compare_versions(OPENSSL_VERSION, "1.0.2") >= 0 || compare_versions(LIBRESSL_VERSION, "2.5.0") >= 0 %} + {% if compare_versions(OPENSSL_VERSION, "1.0.2") >= 0 || compare_versions(LIBRESSL_VERSION, "2.1.0") >= 0 %} alias ALPNCallback = (SSL, Char**, Char*, Char*, Int, Void*) -> Int fun ssl_get0_alpn_selected = SSL_get0_alpn_selected(handle : SSL, data : Char**, len : LibC::UInt*) : Void @@ -272,7 +280,7 @@ lib LibSSL fun ssl_ctx_set_alpn_protos = SSL_CTX_set_alpn_protos(ctx : SSLContext, protos : Char*, protos_len : Int) : Int {% end %} - {% if compare_versions(OPENSSL_VERSION, "1.0.2") >= 0 %} + {% if compare_versions(OPENSSL_VERSION, "1.0.2") >= 0 || compare_versions(LIBRESSL_VERSION, "2.7.0") >= 0 %} alias X509VerifyParam = LibCrypto::X509VerifyParam fun dtls_method = DTLS_method : SSLMethod @@ -282,7 +290,7 @@ lib LibSSL fun ssl_ctx_set1_param = SSL_CTX_set1_param(ctx : SSLContext, param : X509VerifyParam) : Int {% end %} - {% if compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 %} + {% if compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 || compare_versions(LIBRESSL_VERSION, "3.6.0") >= 0 %} fun ssl_ctx_set_security_level = SSL_CTX_set_security_level(ctx : SSLContext, level : Int) : Void fun ssl_ctx_get_security_level = SSL_CTX_get_security_level(ctx : SSLContext) : Int {% end %} @@ -293,7 +301,7 @@ lib LibSSL {% end %} end -{% unless compare_versions(LibSSL::OPENSSL_VERSION, "1.1.0") >= 0 %} +{% if LibSSL.has_method?(:ssl_library_init) %} LibSSL.ssl_library_init LibSSL.ssl_load_error_strings LibCrypto.openssl_add_all_algorithms diff --git a/src/regex/lib_pcre.cr b/src/regex/lib_pcre.cr index da3ac3beb764..8502e5531d3e 100644 --- a/src/regex/lib_pcre.cr +++ b/src/regex/lib_pcre.cr @@ -1,3 +1,8 @@ +# Supported library versions: +# +# * libpcre +# +# See https://crystal-lang.org/reference/man/required_libraries.html#regular-expression-engine @[Link("pcre", pkg_config: "libpcre")] {% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %} @[Link(dll: "pcre.dll")] diff --git a/src/regex/lib_pcre2.cr b/src/regex/lib_pcre2.cr index 651a1c95bef2..6f45a4465219 100644 --- a/src/regex/lib_pcre2.cr +++ b/src/regex/lib_pcre2.cr @@ -1,3 +1,8 @@ +# Supported library versions: +# +# * libpcre2 (recommended: 10.36+) +# +# See https://crystal-lang.org/reference/man/required_libraries.html#regular-expression-engine @[Link("pcre2-8", pkg_config: "libpcre2-8")] {% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %} @[Link(dll: "pcre2-8.dll")] diff --git a/src/socket/address.cr b/src/socket/address.cr index 20fca43544e6..bac36088152f 100644 --- a/src/socket/address.cr +++ b/src/socket/address.cr @@ -21,6 +21,26 @@ class Socket end end + # :ditto: + def self.from(sockaddr : LibC::Sockaddr*) : Address + case family = Family.new(sockaddr.value.sa_family) + when Family::INET6 + sockaddr = sockaddr.as(LibC::SockaddrIn6*) + + IPAddress.new(sockaddr, sizeof(typeof(sockaddr))) + when Family::INET + sockaddr = sockaddr.as(LibC::SockaddrIn*) + + IPAddress.new(sockaddr, sizeof(typeof(sockaddr))) + when Family::UNIX + sockaddr = sockaddr.as(LibC::SockaddrUn*) + + UNIXAddress.new(sockaddr, sizeof(typeof(sockaddr))) + else + raise "Unsupported family type: #{family} (#{family.value})" + end + end + # Parses a `Socket::Address` from an URI. # # Supported formats: @@ -113,6 +133,22 @@ class Socket end end + # :ditto: + def self.from(sockaddr : LibC::Sockaddr*) : IPAddress + case family = Family.new(sockaddr.value.sa_family) + when Family::INET6 + sockaddr = sockaddr.as(LibC::SockaddrIn6*) + + new(sockaddr, sizeof(typeof(sockaddr))) + when Family::INET + sockaddr = sockaddr.as(LibC::SockaddrIn*) + + new(sockaddr, sizeof(typeof(sockaddr))) + else + raise "Unsupported family type: #{family} (#{family.value})" + end + end + # Parses a `Socket::IPAddress` from an URI. # # It expects the URI to include `://:` where `scheme` as @@ -750,6 +786,17 @@ class Socket {% end %} end + # :ditto: + def self.from(sockaddr : LibC::Sockaddr*) : UNIXAddress + {% if flag?(:wasm32) %} + raise NotImplementedError.new "Socket::UNIXAddress.from" + {% else %} + sockaddr = sockaddr.as(LibC::SockaddrUn*) + + new(sockaddr, sizeof(typeof(sockaddr))) + {% end %} + end + # Parses a `Socket::UNIXAddress` from an URI. # # It expects the URI to include `://` where `scheme` as well diff --git a/src/string.cr b/src/string.cr index 7507e3b7249e..07d65f10dbd4 100644 --- a/src/string.cr +++ b/src/string.cr @@ -317,7 +317,9 @@ class String # * **whitespace**: if `true`, leading and trailing whitespaces are allowed # * **underscore**: if `true`, underscores in numbers are allowed # * **prefix**: if `true`, the prefixes `"0x"`, `"0o"` and `"0b"` override the base - # * **strict**: if `true`, extraneous characters past the end of the number are disallowed + # * **strict**: if `true`, extraneous characters past the end of the number + # are disallowed, unless **whitespace** is also `true` and all the trailing + # characters past the number are whitespaces # * **leading_zero_is_octal**: if `true`, then a number prefixed with `"0"` will be treated as an octal # # ``` @@ -692,7 +694,9 @@ class String # # Options: # * **whitespace**: if `true`, leading and trailing whitespaces are allowed - # * **strict**: if `true`, extraneous characters past the end of the number are disallowed + # * **strict**: if `true`, extraneous characters past the end of the number + # are disallowed, unless **whitespace** is also `true` and all the trailing + # characters past the number are whitespaces # # ``` # "123.45e1".to_f # => 1234.5 @@ -717,7 +721,9 @@ class String # # Options: # * **whitespace**: if `true`, leading and trailing whitespaces are allowed - # * **strict**: if `true`, extraneous characters past the end of the number are disallowed + # * **strict**: if `true`, extraneous characters past the end of the number + # are disallowed, unless **whitespace** is also `true` and all the trailing + # characters past the number are whitespaces # # ``` # "123.45e1".to_f? # => 1234.5 @@ -1661,12 +1667,12 @@ class String case to_unsafe[bytesize - 1] when '\n' if bytesize > 1 && to_unsafe[bytesize - 2] === '\r' - unsafe_byte_slice_string(0, bytesize - 2) + unsafe_byte_slice_string(0, bytesize - 2, @length > 0 ? @length - 2 : 0) else - unsafe_byte_slice_string(0, bytesize - 1) + unsafe_byte_slice_string(0, bytesize - 1, @length > 0 ? @length - 1 : 0) end when '\r' - unsafe_byte_slice_string(0, bytesize - 1) + unsafe_byte_slice_string(0, bytesize - 1, @length > 0 ? @length - 1 : 0) else self end @@ -1798,11 +1804,7 @@ class String def rchop? : String? return if empty? - if to_unsafe[bytesize - 1] < 0x80 || single_byte_optimizable? - return unsafe_byte_slice_string(0, bytesize - 1) - end - - self[0, size - 1] + unsafe_byte_slice_string(0, Char::Reader.new(at_end: self).pos, @length > 0 ? @length - 1 : 0) end # Returns a new `String` with *suffix* removed from the end of the string if possible, else returns `nil`. @@ -5552,12 +5554,12 @@ class String Slice.new(to_unsafe + byte_offset, bytesize - byte_offset, read_only: true) end - protected def unsafe_byte_slice_string(byte_offset) - String.new(unsafe_byte_slice(byte_offset)) + protected def unsafe_byte_slice_string(byte_offset, *, size = 0) + String.new(to_unsafe + byte_offset, bytesize - byte_offset, size) end - protected def unsafe_byte_slice_string(byte_offset, count) - String.new(unsafe_byte_slice(byte_offset, count)) + protected def unsafe_byte_slice_string(byte_offset, count, size = 0) + String.new(to_unsafe + byte_offset, count, size) end protected def self.char_bytes_and_bytesize(char : Char) diff --git a/src/wait_group.cr b/src/wait_group.cr index c1ebe67bf508..cf1ca8900e8f 100644 --- a/src/wait_group.cr +++ b/src/wait_group.cr @@ -73,9 +73,9 @@ class WaitGroup # wg.spawn { do_something } # wg.wait # ``` - def spawn(&block) : Fiber + def spawn(*, name : String? = nil, &block) : Fiber add - ::spawn do + ::spawn(name: name) do block.call ensure done diff --git a/src/winerror.cr b/src/winerror.cr index fbb2fb553873..844df5b07315 100644 --- a/src/winerror.cr +++ b/src/winerror.cr @@ -60,6 +60,9 @@ enum WinError : UInt32 # using the current default `LANGID`. # # On non-win32 platforms the result is always an empty string. + # + # NOTE: The result may depend on the current system locale. Specs and + # comparisons should use `#value` instead of this method. def message : String {% if flag?(:win32) %} unsafe_message { |slice| String.from_utf16(slice).strip } diff --git a/src/xml/libxml2.cr b/src/xml/libxml2.cr index fbfb0702faef..05b255ba23dc 100644 --- a/src/xml/libxml2.cr +++ b/src/xml/libxml2.cr @@ -4,6 +4,11 @@ require "./parser_options" require "./html_parser_options" require "./save_options" +# Supported library versions: +# +# * libxml2 +# +# See https://crystal-lang.org/reference/man/required_libraries.html#other-stdlib-libraries @[Link("xml2", pkg_config: "libxml-2.0")] {% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %} @[Link(dll: "libxml2.dll")] diff --git a/src/yaml/lib_yaml.cr b/src/yaml/lib_yaml.cr index 0b4248afc793..d1527db63be2 100644 --- a/src/yaml/lib_yaml.cr +++ b/src/yaml/lib_yaml.cr @@ -1,5 +1,10 @@ require "./enums" +# Supported library versions: +# +# * libyaml +# +# See https://crystal-lang.org/reference/man/required_libraries.html#other-stdlib-libraries @[Link("yaml", pkg_config: "yaml-0.1")] {% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %} @[Link(dll: "yaml.dll")]