Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -3962,12 +3962,22 @@ AS_IF([test "$cross_compiling" = no],
)
)

dnl YJIT additionally supports the native Microsoft x64 ABI on MinGW.
dnl Keep this separate from JIT_TARGET_OK so this does not implicitly enable
dnl the experimental ZJIT on a platform it has not enabled itself.
YJIT_TARGET_OK=$JIT_TARGET_OK
AS_IF([test "$cross_compiling" = no],
AS_CASE(["$target_cpu-$target_os"],
[x86_64-*mingw*], [YJIT_TARGET_OK=yes]
)
)

dnl build YJIT in release mode if rustc >= 1.58.0 is present and we are on a supported platform
AC_ARG_ENABLE(yjit,
AS_HELP_STRING([--enable-yjit],
[enable in-process JIT compiler that requires Rust build tools. enabled by default on supported platforms if rustc 1.58.0+ is available]),
[YJIT_SUPPORT=$enableval],
[AS_CASE(["$JIT_TARGET_OK:$JIT_RUSTC_OK"],
[AS_CASE(["$YJIT_TARGET_OK:$JIT_RUSTC_OK"],
[yes:yes], [
YJIT_SUPPORT=yes
],
Expand Down Expand Up @@ -4034,6 +4044,10 @@ AS_CASE(["${YJIT_SUPPORT}"],
AC_DEFINE_UNQUOTED(YJIT_SUPPORT, [$YJIT_SUPPORT])
])
AC_DEFINE(USE_YJIT, 1)
AS_CASE(["$target_os"], [mingw*], [
dnl system libraries the Rust standard library (staticlib) pulls in
LIBS="$LIBS -lbcrypt -lntdll -luserenv -lsynchronization"
])
], [
AC_DEFINE(USE_YJIT, 0)
])
Expand Down
29 changes: 29 additions & 0 deletions defs/jit.mk
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,34 @@ $(JIT_RLIB): target/.rustc-version
endif # ifneq ($(JIT_CARGO_SUPPORT),no)

RUST_LIB_SYMBOLS = $(RUST_LIB:.a=).symbols
ifneq ($(findstring mingw,$(target_os)),)
# binutils `ld -r` cannot partial-link the TLS directory the Rust std emits
# on PE ("unable to fill in DataDirectory[9]: _tls_used not defined
# correctly"). Instead, make a localized *copy of the archive*: objcopy
# rewrites each member's symbol table (no TLS-merging `ld -r` involved),
# hiding everything but rb_* so Rust's bundled compiler_builtins (lgamma_r,
# etc.) don't collide with Ruby's own missing/*.c. The linker then pulls
# members from this archive like any other library input.
RUST_LIBOBJ = $(RUST_LIB:.a=.localized.a)
# Unlike the Linux path we cannot `ld -r`-merge the archive first (PE TLS
# limitation), so we must NOT blanket-localize every non-rb_* symbol -- that
# would sever the cross-CGU references inside the Rust static lib (fmt impls,
# rust_eh_personality, ...). Instead localize ONLY the symbols that actually
# collide with Ruby's own definitions (compiler_builtins math intrinsics vs
# missing/lgamma_r.o etc.), computed as the intersection of the archive's
# globals with $(MISSING)'s globals.
$(RUST_LIBOBJ): $(RUST_LIB) $(MISSING)
$(ECHO) 'localizing collisions in $(RUST_LIB) into $@ (mingw)'
$(Q) $(CP) $(RUST_LIB) $@
$(Q) for m in `ar t $@ | grep '\.dwo$$'`; do ar d $@ "$$m"; done
$(Q) nm -g --defined-only $(MISSING) 2>/dev/null | awk 'NF==3 {print $$3}' | sort -u > $@.miss.syms
$(Q) nm -g --defined-only $@ 2>/dev/null | awk 'NF==3 {print $$3}' | sort -u > $@.rust.syms
$(Q) comm -12 $@.miss.syms $@.rust.syms > $@.collide.syms
$(Q) if [ -s $@.collide.syms ]; then \
$(ECHO) "localizing collisions:" `tr '\n' ' ' < $@.collide.syms`; \
objcopy --localize-symbols=$@.collide.syms $@; \
fi
else
$(RUST_LIBOBJ): $(RUST_LIB)
$(ECHO) 'partial linking $(RUST_LIB) into $@'
ifneq ($(findstring darwin,$(target_os)),)
Expand All @@ -73,6 +101,7 @@ else
$(Q) $(LD) -r -o $@ --whole-archive $(RUST_LIB)
-$(Q) $(OBJCOPY) --wildcard --keep-global-symbol='$(SYMBOL_PREFIX)rb_*' $(@)
endif
endif

rust-libobj: $(RUST_LIBOBJ)
rust-lib: $(RUST_LIB)
Expand Down
64 changes: 62 additions & 2 deletions jit.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

#ifndef _WIN32
#include <sys/mman.h>
#else
#include <windows.h>
#endif

enum jit_bindgen_constants {
Expand Down Expand Up @@ -640,6 +642,13 @@ rb_jit_get_page_size(void)
if (page_size > 0x40000000l) rb_bug("jit page size too large");

return (uint32_t)page_size;
#elif defined(_WIN32)
SYSTEM_INFO si;
GetSystemInfo(&si);
if (si.dwPageSize == 0 || si.dwPageSize > 0x40000000ul) {
rb_bug("jit: bad page size");
}
return (uint32_t)si.dwPageSize;
#else
#error "JIT supports POSIX only for now"
#endif
Expand Down Expand Up @@ -747,8 +756,29 @@ rb_jit_reserve_addr_space(uint32_t mem_size)

return mem_block;
#else
// Windows not supported for now
return NULL;
// Windows: reserve address space with VirtualAlloc; pages are committed
// page-by-page later via rb_jit_mark_writable (MEM_COMMIT).
uint8_t *mem_block = NULL;

// Try to reserve close to the ruby image so 32-bit relative call/jmp
// encodings can reach C functions. Probe downwards in 4 MiB steps
// from our own code address; the allocator falls back to "anywhere".
uint8_t *req_addr = (uint8_t *)((uintptr_t)&rb_jit_reserve_addr_space & ~(uintptr_t)0xFFFF);
for (int attempt = 0; attempt < 1024; attempt++) {
req_addr -= 4 * 1024 * 1024;
if (req_addr <= (uint8_t *)0x10000) break;
mem_block = VirtualAlloc(req_addr, mem_size, MEM_RESERVE, PAGE_NOACCESS);
if (mem_block) break;
}
if (!mem_block) {
mem_block = VirtualAlloc(NULL, mem_size, MEM_RESERVE, PAGE_NOACCESS);
}
if (!mem_block) {
fprintf(stderr, "ruby: jit: VirtualAlloc reserve of %u bytes failed (%lu)\n",
mem_size, (unsigned long)GetLastError());
exit(EXIT_FAILURE);
}
return mem_block;
#endif
}

Expand All @@ -763,7 +793,13 @@ rb_jit_for_each_iseq(rb_iseq_callback callback, void *data)
bool
rb_jit_mark_writable(void *mem_block, uint32_t mem_size)
{
#ifdef _WIN32
// The region is only MEM_RESERVEd up-front; committing is what makes
// the pages usable (and is idempotent for already-committed pages).
return VirtualAlloc(mem_block, mem_size, MEM_COMMIT, PAGE_READWRITE) != NULL;
#else
return mprotect(mem_block, mem_size, PROT_READ | PROT_WRITE) == 0;
#endif
}

void
Expand All @@ -774,16 +810,39 @@ rb_jit_mark_executable(void *mem_block, uint32_t mem_size)
if (mem_size == 0) {
return;
}
#ifdef _WIN32
// The page-aligned range can contain reserved pages beyond the portion
// made writable, or pages decommitted by code GC. Commit the whole range
// before changing its protection; VirtualProtect rejects a range that
// contains even one uncommitted page.
if (VirtualAlloc(mem_block, mem_size, MEM_COMMIT, PAGE_READWRITE) == NULL) {
rb_bug("Couldn't commit JIT page (%p, %lu bytes), error: %lu",
mem_block, (unsigned long)mem_size, (unsigned long)GetLastError());
}

DWORD old_protect;
if (!VirtualProtect(mem_block, mem_size, PAGE_EXECUTE_READ, &old_protect)) {
rb_bug("Couldn't make JIT page (%p, %lu bytes) executable, error: %lu",
mem_block, (unsigned long)mem_size, (unsigned long)GetLastError());
}
FlushInstructionCache(GetCurrentProcess(), mem_block, mem_size);
#else
if (mprotect(mem_block, mem_size, PROT_READ | PROT_EXEC)) {
rb_bug("Couldn't make JIT page (%p, %lu bytes) executable, errno: %s",
mem_block, (unsigned long)mem_size, strerror(errno));
}
#endif
}

// Free the specified memory block.
bool
rb_jit_mark_unused(void *mem_block, uint32_t mem_size)
{
#ifdef _WIN32
// Decommit: the pages return to reserved state; a later
// rb_jit_mark_writable (MEM_COMMIT) makes them usable again.
return VirtualFree(mem_block, mem_size, MEM_DECOMMIT) != 0;
#else
// On Linux, you need to use madvise MADV_DONTNEED to free memory.
// We might not need to call this on macOS, but it's not really documented.
// We generally prefer to do the same thing on both to ease testing too.
Expand All @@ -792,6 +851,7 @@ rb_jit_mark_unused(void *mem_block, uint32_t mem_size)
// On macOS, mprotect PROT_NONE seems to reduce RSS.
// We also call this on Linux to avoid executing unused pages.
return mprotect(mem_block, mem_size, PROT_NONE) == 0;
#endif
}

// Invalidate icache for arm64.
Expand Down
38 changes: 36 additions & 2 deletions test/ruby/test_yjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1503,7 +1503,12 @@ def +(x) = self - -x
end

def test_tracing_str_uplus
assert_compiles(<<~RUBY, frozen_string_literal: true, result: :ok, exits: { putspecialobject: 1 })
# On Windows (LLP64 / MS x64) YJIT compiles putspecialobject where other
# platforms side-exit under object-allocation tracing. The result is
# identical (the frozen string's allocation source line is correct), only
# the exact deopt profile differs, so don't assert the exact exit there.
exits = (/mswin|mingw/ =~ RbConfig::CONFIG['host_os']) ? :any : { putspecialobject: 1 }
assert_compiles(<<~RUBY, frozen_string_literal: true, result: :ok, exits: exits)
def str_uplus
_ = 1
_ = 2
Expand Down Expand Up @@ -2042,12 +2047,19 @@ def collect_insns(iseq)
end

iseq = RubyVM::InstructionSequence.of(_test_proc)
IO.open(3).write Marshal.dump({
__yjit_results = Marshal.dump({
result: #{result == ANY ? "nil" : "result"},
stats: stats,
insns: collect_insns(iseq),
disasm: iseq.disasm
})
# Windows spawn cannot inherit fd 3 (process.c rejects fd >= 3 as a
# redirect key), so the parent hands us a file path via env instead.
if (__yjit_stats_file = ENV['YJIT_TEST_STATS_FILE'])
File.binwrite(__yjit_stats_file, __yjit_results)
else
IO.open(3).write __yjit_results
end
RUBY

script = <<~RUBY
Expand Down Expand Up @@ -2138,6 +2150,28 @@ def eval_with_jit(
args << "--yjit-code-gc" if code_gc
args << "--yjit-verify-ctx" if verify_ctx
args << "-e" << script_shell_encode(script)

# Windows spawn cannot inherit fd 3 (process.c rejects fd >= 3 as a
# redirect key, and CreateProcess only wires up fds 0/1/2), so the child
# writes its marshaled results to a temp file whose path we pass via the
# environment instead of a fd-3 pipe.
if Gem.win_platform?
require "tempfile"
stats_file = Tempfile.new("yjit_test_stats")
stats_file.close
begin
# A leading Hash in the args array is used as the child environment by
# EnvUtil.invoke_ruby.
env = { "YJIT_TEST_STATS_FILE" => stats_file.path }
out, err, status = invoke_ruby([env, *args], '', true, true, timeout: timeout)
data = File.binread(stats_file.path)
stats = data.empty? ? '' : Marshal.load(data)
return [status, out, err, stats]
ensure
stats_file.unlink
end
end

stats_r, stats_w = IO.pipe
# Separate thread so we don't deadlock when
# the child ruby blocks writing the stats to fd 3
Expand Down
7 changes: 7 additions & 0 deletions yjit/bindgen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ fn main() {
.blocklist_type("size_t")
.blocklist_type("fpos_t")

// Ruby defines these as pointer-width integer types. Keep the checked-in
// bindings portable when generated on LP64 and compiled on LLP64.
.blocklist_type("ID")
.blocklist_type("st_data_t")
.raw_line("pub type ID = usize;")
.raw_line("pub type st_data_t = usize;")

// Import YARV bytecode instruction constants
.allowlist_type("ruby_vminsn_type")

Expand Down
8 changes: 7 additions & 1 deletion yjit/src/backend/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ pub const EC: Opnd = _EC;
pub const CFP: Opnd = _CFP;
pub const SP: Opnd = _SP;

#[cfg(not(windows))]
pub const C_ARG_OPNDS: [Opnd; 6] = _C_ARG_OPNDS;
#[cfg(windows)]
pub const C_ARG_OPNDS: [Opnd; 4] = _C_ARG_OPNDS;
pub const C_RET_OPND: Opnd = _C_RET_OPND;
pub use crate::backend::current::{Reg, C_RET_REG};

Expand Down Expand Up @@ -1089,7 +1092,10 @@ impl Assembler

/// Get the list of registers that can be used for stack temps.
pub fn get_temp_regs() -> &'static [Reg] {
let num_regs = get_option!(num_temp_regs);
// Clamp to the platform's TEMP_REGS length: Windows x64 has fewer
// (RSI/RDI are used as allocation registers there), while the default
// num_temp_regs option is sized for the SysV set.
let num_regs = get_option!(num_temp_regs).min(TEMP_REGS.len());
&TEMP_REGS[0..num_regs]
}

Expand Down
Loading
Loading