Current section

Files

Jump to
zigler priv beam io.zig
Raw

priv/beam/io.zig

//! BEAM-based Io implementation for Zig 0.16.0
//!
//! This module provides an std.Io interface backed by std.Io.Threaded
//! configured for BEAM NIF use, with BEAM-specific overrides:
//!
//! - Time: Uses enif_monotonic_time/enif_time_offset for BEAM-consistent time
//! - Stderr: No locking (can't coordinate with BEAM's stderr)
//!
//! Configuration:
//! - Uses beam.allocator for memory allocation
//! - environ is disabled - use Erlang's os:getenv/1 or Elixir's System.get_env/1
//! and pass the value into the NIF function as a parameter
//! - Signal handlers are disabled (BEAM manages signals)
const std = @import("std");
const builtin = @import("builtin");
const beam = @import("beam.zig");
const e = @import("erl_nif");
const Io = std.Io;
const Threaded = Io.Threaded;
const Dir = Io.Dir;
const File = Io.File;
const windows = std.os.windows;
/// Global Threaded instance - initialized in on_load
var global_threaded: ?Threaded = null;
/// Our custom vtable - copied from Threaded with selective overrides
var vtable: Io.VTable = undefined;
/// Initialize the global Io instance.
/// Called from on_load and on_upgrade.
/// Idempotent - safe to call multiple times.
pub fn initGlobal() void {
if (global_threaded != null) return;
global_threaded = .{
.allocator = beam.allocator,
.stack_size = std.Thread.SpawnConfig.default_stack_size,
.async_limit = .nothing,
.cpu_count_error = null,
.concurrent_limit = .nothing,
.old_sig_io = undefined,
.old_sig_pipe = undefined,
.have_signal_handler = false,
.argv0 = .empty,
.environ_initialized = false, // Use os:getenv/System.get_env and pass as parameter
.environ = .{ .process_environ = .{ .block = .empty } },
.worker_threads = .init(null),
.disable_memory_mapping = false,
};
// Copy Threaded's vtable as our base
vtable = global_threaded.?.io().vtable.*;
// Apply BEAM-specific overrides
vtable.now = now;
vtable.clockResolution = clockResolution;
vtable.lockStderr = lockStderr;
vtable.tryLockStderr = tryLockStderr;
vtable.unlockStderr = unlockStderr;
// Windows-specific: Override dirOpenFile to bypass sliceToPrefixedFileW
// which hangs when called from threads created by enif_thread_create.
if (builtin.os.tag == .windows) {
vtable.dirOpenFile = dirOpenFileWindows;
}
}
pub fn deinitGlobal() void {
if (global_threaded) |*g| {
g.deinit();
global_threaded = null;
}
}
/// Get the std.Io interface with our custom vtable.
/// The allocator is used for Io operations that require allocation.
pub fn get(allocator: std.mem.Allocator) Io {
global_threaded.?.allocator = allocator;
return .{
.userdata = @ptrCast(&global_threaded.?),
.vtable = &vtable,
};
}
// ============================================================================
// BEAM-specific implementations
// ============================================================================
/// Time using BEAM's enif_monotonic_time / enif_time_offset
/// This ensures time is consistent with BEAM's view of time.
fn now(userdata: ?*anyopaque, clock: Io.Clock) Io.Timestamp {
const beam_time: i64 = switch (clock) {
// awake = monotonic time (excluding suspend)
// boot = monotonic time (including suspend)
.awake, .boot => e.enif_monotonic_time(e.ERL_NIF_NSEC),
// real = wall clock time
.real => blk: {
// BEAM system time = monotonic + offset
const mono = e.enif_monotonic_time(e.ERL_NIF_NSEC);
const offset = e.enif_time_offset(e.ERL_NIF_NSEC);
break :blk mono + offset;
},
// CPU time - no BEAM equivalent, fall back to Threaded
.cpu_process, .cpu_thread => {
const threaded: *Threaded = @ptrCast(@alignCast(userdata.?));
return threaded.io().vtable.now(userdata, clock);
},
};
return .{ .nanoseconds = @as(i96, beam_time) };
}
/// Clock resolution - BEAM time functions provide nanosecond precision
fn clockResolution(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.ResolutionError!Io.Duration {
switch (clock) {
.cpu_process, .cpu_thread => {
// Fall back to Threaded for CPU time clocks
const threaded: *Threaded = @ptrCast(@alignCast(userdata.?));
return threaded.io().vtable.clockResolution(userdata, clock);
},
else => {
// BEAM time resolution is nanosecond
return .{ .nanoseconds = 1 };
},
}
}
/// Stderr - no locking (can't coordinate with BEAM's stderr)
fn lockStderr(userdata: ?*anyopaque, mode: ?Io.Terminal.Mode) Io.Cancelable!Io.LockedStderr {
const threaded: *Threaded = @ptrCast(@alignCast(userdata.?));
// Return stderr without acquiring any lock
return .{
.file_writer = &threaded.stderr_writer,
.terminal_mode = mode orelse threaded.stderr_mode,
};
}
fn tryLockStderr(userdata: ?*anyopaque, mode: ?Io.Terminal.Mode) Io.Cancelable!?Io.LockedStderr {
// Always succeeds since we don't lock
return try lockStderr(userdata, mode);
}
fn unlockStderr(_: ?*anyopaque) void {
// No-op since we don't lock
}
/// Windows-specific dirOpenFile that bypasses sliceToPrefixedFileW.
/// The standard path conversion hangs when called from threads created by
/// enif_thread_create. This implementation directly converts to NT paths.
fn dirOpenFileWindows(
userdata: ?*anyopaque,
dir: Dir,
sub_path: []const u8,
flags: Dir.OpenFileOptions,
) File.OpenError!File {
_ = userdata; // Unused - we go directly to dirOpenFileWtf16
// Convert WTF8 path to WTF16
var path_buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
const path_len = std.unicode.wtf8ToWtf16Le(&path_buffer, sub_path) catch {
return error.BadPathName;
};
// Check if path is absolute (drive letter like C:\ or UNC like \\server)
const is_absolute = blk: {
if (path_len >= 3 and path_buffer[1] == ':' and
(path_buffer[2] == '\\' or path_buffer[2] == '/'))
{
break :blk true; // Drive absolute: C:\...
}
if (path_len >= 2 and path_buffer[0] == '\\' and path_buffer[1] == '\\') {
break :blk true; // UNC: \\server\...
}
break :blk false;
};
if (is_absolute) {
// For absolute paths, add NT prefix \??\ and use null dir handle
var nt_path_buffer: [4 + windows.PATH_MAX_WIDE]u16 = undefined;
nt_path_buffer[0..4].* = .{ '\\', '?', '?', '\\' };
@memcpy(nt_path_buffer[4..][0..path_len], path_buffer[0..path_len]);
return Threaded.dirOpenFileWtf16(null, nt_path_buffer[0 .. 4 + path_len], flags);
} else {
// For relative paths, use the dir handle directly
// The standard implementation should work for relative paths
return Threaded.dirOpenFileWtf16(dir.handle, path_buffer[0..path_len], flags);
}
}
// Note: Other functions we considered but left with Threaded defaults:
//
// - sleep: Could use enif_consume_timeslice for cooperative sleeping,
// but that changes semantics significantly. Threaded uses posix nanosleep.
//
// - futexWait/futexWake: Could implement with BEAM cond/mutex, but the
// semantics are complex. Threaded's posix futex implementation works.
//
// - random/randomSecure: Could use enif_make_unique_integer, but
// Threaded's implementation is more appropriate for crypto-quality random.
//
// - environ: Disabled via environ_initialized=false. Use Erlang's os:getenv/1
// or Elixir's System.get_env/1 and pass values into the NIF as parameters.