lib_io.c

/*
 * Copyright (C) 2025 Jo-Philipp Wich <jo@mein.io>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/**
 * # I/O Operations
 *
 * The `io` module provides object-oriented access to UNIX file descriptors.
 *
 * Functions can be individually imported and directly accessed using the
 * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#named_import named import}
 * syntax:
 *
 *   ```
 *   import { open, O_RDWR } from 'io';
 *
 *   let handle = open('/tmp/test.txt', O_RDWR);
 *   handle.write('Hello World\n');
 *   handle.close();
 *   ```
 *
 * Alternatively, the module namespace can be imported
 * using a wildcard import statement:
 *
 *   ```
 *   import * as io from 'io';
 *
 *   let handle = io.open('/tmp/test.txt', io.O_RDWR);
 *   handle.write('Hello World\n');
 *   handle.close();
 *   ```
 *
 * Additionally, the io module namespace may also be imported by invoking
 * the `ucode` interpreter with the `-lio` switch.
 *
 * @module io
 */

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>

#if defined(__linux__)
#define HAS_IOCTL
#endif

#ifdef HAS_IOCTL
#include <sys/ioctl.h>

#define IOC_DIR_NONE	(_IOC_NONE)
#define IOC_DIR_READ	(_IOC_READ)
#define IOC_DIR_WRITE	(_IOC_WRITE)
#define IOC_DIR_RW		(_IOC_READ | _IOC_WRITE)

#endif

#include "ucode/module.h"
#include "ucode/platform.h"

#define err_return(err) do { \
	uc_vm_registry_set(vm, "io.last_error", ucv_int64_new(err)); \
	return NULL; \
} while(0)

typedef struct {
	int fd;
	bool close_on_free;
} uc_io_handle_t;

static bool
get_fd_from_value(uc_vm_t *vm, uc_value_t *val, int *fd)
{
	uc_io_handle_t *handle;
	uc_value_t *fn;
	int64_t n;

	/* Check if it's an io.handle resource */
	handle = ucv_resource_data(val, "io.handle");

	if (handle) {
		if (handle->fd < 0)
			err_return(EBADF);

		*fd = handle->fd;

		return true;
	}

	/* Try calling fileno() method */
	fn = ucv_property_get(val, "fileno");
	errno = 0;

	if (ucv_is_callable(fn)) {
		uc_vm_stack_push(vm, ucv_get(val));
		uc_vm_stack_push(vm, ucv_get(fn));

		if (uc_vm_call(vm, true, 0) != EXCEPTION_NONE)
			err_return(EBADF);

		val = uc_vm_stack_pop(vm);
		n = ucv_int64_get(val);
		ucv_put(val);
	}
	else {
		n = ucv_int64_get(val);
	}

	if (errno || n < 0 || n > (int64_t)INT_MAX)
		err_return(errno ? errno : EBADF);

	*fd = n;

	return true;
}

/**
 * Query error information.
 *
 * Returns a string containing a description of the last occurred error or
 * `null` if there is no error information.
 *
 * @function module:io#error
 *
 * @returns {?string}
 *
 * @example
 * // Trigger an error
 * io.open('/path/does/not/exist');
 *
 * // Print error (should yield "No such file or directory")
 * print(io.error(), "\n");
 */
static uc_value_t *
uc_io_error(uc_vm_t *vm, size_t nargs)
{
	int last_error = ucv_int64_get(uc_vm_registry_get(vm, "io.last_error"));

	if (last_error == 0)
		return NULL;

	uc_vm_registry_set(vm, "io.last_error", ucv_int64_new(0));

	return ucv_string_new(strerror(last_error));
}

/**
 * Represents a handle for interacting with a file descriptor.
 *
 * @class module:io.handle
 * @hideconstructor
 *
 * @borrows module:io#error as module:io.handle#error
 *
 * @see {@link module:io#new|new()}
 * @see {@link module:io#open|open()}
 * @see {@link module:io#from|from()}
 *
 * @example
 *
 * const handle = io.open(…);
 *
 * handle.read(…);
 * handle.write(…);
 *
 * handle.seek(…);
 * handle.tell();
 *
 * handle.fileno();
 *
 * handle.close();
 *
 * handle.error();
 */

/**
 * Reads data from the file descriptor.
 *
 * Reads up to the specified number of bytes from the file descriptor.
 *
 * Returns a string containing the read data.
 *
 * Returns an empty string on EOF.
 *
 * Returns `null` if a read error occurred.
 *
 * @function module:io.handle#read
 *
 * @param {number} length
 * The maximum number of bytes to read.
 *
 * @returns {?string}
 *
 * @example
 * const handle = io.open('/tmp/test.txt', O_RDONLY);
 * const data = handle.read(1024);
 */
static uc_value_t *
uc_io_read(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *limit = uc_fn_arg(0);
	uc_value_t *rv = NULL;
	uc_io_handle_t *handle;
	int fd;
	int64_t len;
	ssize_t rlen;
	char *buf;

	handle = uc_fn_thisval("io.handle");

	if (!handle || handle->fd < 0)
		err_return(EBADF);

	fd = handle->fd;

	if (ucv_type(limit) != UC_INTEGER)
		err_return(EINVAL);

	len = ucv_int64_get(limit);

	if (len <= 0)
		return ucv_string_new_length("", 0);

	if (len > SSIZE_MAX)
		len = SSIZE_MAX;

	buf = xalloc(len);

	rlen = read(fd, buf, len);

	if (rlen < 0) {
		free(buf);
		err_return(errno);
	}

	rv = ucv_string_new_length(buf, rlen);
	free(buf);

	return rv;
}

/**
 * Writes data to the file descriptor.
 *
 * Writes the given data to the file descriptor. Non-string values are
 * converted to strings before being written.
 *
 * Returns the number of bytes written.
 *
 * Returns `null` if a write error occurred.
 *
 * @function module:io.handle#write
 *
 * @param {*} data
 * The data to write.
 *
 * @returns {?number}
 *
 * @example
 * const handle = io.open('/tmp/test.txt', O_WRONLY | O_CREAT);
 * handle.write('Hello World\n');
 */
static uc_value_t *
uc_io_write(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *data = uc_fn_arg(0);
	uc_io_handle_t *handle;
	ssize_t wlen;
	size_t len;
	char *str;
	int fd;

	handle = uc_fn_thisval("io.handle");

	if (!handle || handle->fd < 0)
		err_return(EBADF);

	fd = handle->fd;

	if (ucv_type(data) == UC_STRING) {
		len = ucv_string_length(data);
		wlen = write(fd, ucv_string_get(data), len);
	}
	else {
		str = ucv_to_jsonstring(vm, data);
		len = str ? strlen(str) : 0;
		wlen = write(fd, str, len);
		free(str);
	}

	if (wlen < 0)
		err_return(errno);

	return ucv_int64_new(wlen);
}

/**
 * Sets the file descriptor position.
 *
 * Sets the file position of the descriptor to the given offset and whence.
 *
 * Returns `true` if the position was successfully set.
 *
 * Returns `null` if an error occurred.
 *
 * @function module:io.handle#seek
 *
 * @param {number} [offset=0]
 * The offset in bytes.
 *
 * @param {number} [whence=0]
 * The position reference.
 *
 * | Whence | Description                                                        |
 * |--------|--------------------------------------------------------------------|
 * | `0`    | The offset is relative to the start of the file (SEEK_SET).       |
 * | `1`    | The offset is relative to the current position (SEEK_CUR).        |
 * | `2`    | The offset is relative to the end of the file (SEEK_END).         |
 *
 * @returns {?boolean}
 *
 * @example
 * const handle = io.open('/tmp/test.txt', O_RDONLY);
 * handle.seek(100, 0);  // Seek to byte 100 from start
 */
static uc_value_t *
uc_io_seek(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *ofs = uc_fn_arg(0);
	uc_value_t *how = uc_fn_arg(1);
	uc_io_handle_t *handle;
	int whence;
	off_t offset;
	int fd;

	handle = uc_fn_thisval("io.handle");

	if (!handle || handle->fd < 0)
		err_return(EBADF);

	fd = handle->fd;

	if (!ofs)
		offset = 0;
	else if (ucv_type(ofs) != UC_INTEGER)
		err_return(EINVAL);
	else
		offset = (off_t)ucv_int64_get(ofs);

	if (!how)
		whence = SEEK_SET;
	else if (ucv_type(how) != UC_INTEGER)
		err_return(EINVAL);
	else
		whence = (int)ucv_int64_get(how);

	if (lseek(fd, offset, whence) < 0)
		err_return(errno);

	return ucv_boolean_new(true);
}

/**
 * Gets the current file descriptor position.
 *
 * Returns the current file position as an integer.
 *
 * Returns `null` if an error occurred.
 *
 * @function module:io.handle#tell
 *
 * @returns {?number}
 *
 * @example
 * const handle = io.open('/tmp/test.txt', O_RDONLY);
 * const pos = handle.tell();
 */
static uc_value_t *
uc_io_tell(uc_vm_t *vm, size_t nargs)
{
	uc_io_handle_t *handle;
	off_t offset;
	int fd;

	handle = uc_fn_thisval("io.handle");

	if (!handle || handle->fd < 0)
		err_return(EBADF);

	fd = handle->fd;

	offset = lseek(fd, 0, SEEK_CUR);

	if (offset < 0)
		err_return(errno);

	return ucv_int64_new(offset);
}

/**
 * Duplicates the file descriptor.
 *
 * Creates a duplicate of the file descriptor using dup(2).
 *
 * Returns a new io.handle for the duplicated descriptor.
 *
 * Returns `null` if an error occurred.
 *
 * @function module:io.handle#dup
 *
 * @returns {?module:io.handle}
 *
 * @example
 * const handle = io.open('/tmp/test.txt', O_RDONLY);
 * const dup_handle = handle.dup();
 */
static uc_value_t *
uc_io_dup(uc_vm_t *vm, size_t nargs)
{
	uc_io_handle_t *handle, *new_handle = NULL;
	uc_value_t *res;
	int fd, newfd;

	handle = uc_fn_thisval("io.handle");

	if (!handle || handle->fd < 0)
		err_return(EBADF);

	fd = handle->fd;

	newfd = dup(fd);

	if (newfd < 0)
		err_return(errno);

	res = ucv_resource_create_ex(vm, "io.handle",
	                             (void **)&new_handle, 0, sizeof(*new_handle));

	if (!new_handle)
		err_return(ENOMEM);

	new_handle->fd = newfd;
	new_handle->close_on_free = true;

	return res;
}

/**
 * Duplicates the file descriptor to a specific descriptor number.
 *
 * Creates a duplicate of the file descriptor to the specified descriptor
 * number using dup2(2). If newfd was previously open, it is silently closed.
 *
 * Returns `true` on success.
 *
 * Returns `null` if an error occurred.
 *
 * @function module:io.handle#dup2
 *
 * @param {number} newfd
 * The target file descriptor number.
 *
 * @returns {?boolean}
 *
 * @example
 * const handle = io.open('/tmp/test.txt', O_WRONLY);
 * handle.dup2(2);  // Redirect stderr to the file
 */
static uc_value_t *
uc_io_dup2(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *newfd_arg = uc_fn_arg(0);
	uc_io_handle_t *handle;
	int fd, newfd;

	handle = uc_fn_thisval("io.handle");

	if (!handle || handle->fd < 0)
		err_return(EBADF);

	fd = handle->fd;

	if (!get_fd_from_value(vm, newfd_arg, &newfd))
		return NULL;

	if (dup2(fd, newfd) < 0)
		err_return(errno);

	return ucv_boolean_new(true);
}

/**
 * Gets the file descriptor number.
 *
 * Returns the underlying file descriptor number.
 *
 * Returns `null` if the handle is closed.
 *
 * @function module:io.handle#fileno
 *
 * @returns {?number}
 *
 * @example
 * const handle = io.open('/tmp/test.txt', O_RDONLY);
 * print(handle.fileno(), "\n");
 */
static uc_value_t *
uc_io_fileno(uc_vm_t *vm, size_t nargs)
{
	uc_io_handle_t *handle;

	handle = uc_fn_thisval("io.handle");

	if (!handle || handle->fd < 0)
		err_return(EBADF);

	return ucv_int64_new(handle->fd);
}

/**
 * Performs fcntl() operations on the file descriptor.
 *
 * Performs the specified fcntl() command on the file descriptor with an
 * optional argument.
 *
 * Returns the result of the fcntl() call. For F_DUPFD and F_DUPFD_CLOEXEC,
 * returns a new io.handle wrapping the duplicated descriptor. For other
 * commands, returns a number (interpretation depends on cmd).
 *
 * Returns `null` if an error occurred.
 *
 * @function module:io.handle#fcntl
 *
 * @param {number} cmd
 * The fcntl command (e.g., F_GETFL, F_SETFL, F_GETFD, F_SETFD, F_DUPFD).
 *
 * @param {number} [arg]
 * Optional argument for the command.
 *
 * @returns {?(number|module:io.handle)}
 *
 * @example
 * const handle = io.open('/tmp/test.txt', O_RDONLY);
 * const flags = handle.fcntl(F_GETFL);
 * handle.fcntl(F_SETFL, flags | O_NONBLOCK);
 * const dup_handle = handle.fcntl(F_DUPFD, 10);  // Returns io.handle
 */
static uc_value_t *
uc_io_fcntl(uc_vm_t *vm, size_t nargs)
{
	uc_io_handle_t *handle, *new_handle = NULL;
	uc_value_t *cmd_arg = uc_fn_arg(0);
	uc_value_t *val_arg = uc_fn_arg(1);
	uc_value_t *res;
	int fd, cmd, ret;
	long arg = 0;

	handle = uc_fn_thisval("io.handle");

	if (!handle || handle->fd < 0)
		err_return(EBADF);

	fd = handle->fd;

	if (ucv_type(cmd_arg) != UC_INTEGER)
		err_return(EINVAL);

	cmd = (int)ucv_int64_get(cmd_arg);

	if (val_arg) {
		if (ucv_type(val_arg) != UC_INTEGER)
			err_return(EINVAL);

		arg = (long)ucv_int64_get(val_arg);
	}

	ret = fcntl(fd, cmd, arg);

	if (ret < 0)
		err_return(errno);

	/* F_DUPFD and F_DUPFD_CLOEXEC return a new fd that we own */
	if (cmd == F_DUPFD
#ifdef F_DUPFD_CLOEXEC
	    || cmd == F_DUPFD_CLOEXEC
#endif
	) {
		res = ucv_resource_create_ex(vm, "io.handle", (void **)&new_handle,
		                             0, sizeof(*new_handle));

		if (!new_handle)
			err_return(ENOMEM);

		new_handle->fd = ret;
		new_handle->close_on_free = true;

		return res;
	}

	return ucv_int64_new(ret);
}

#ifdef HAS_IOCTL

/**
 * Performs an ioctl operation on the file descriptor.
 *
 * The direction parameter specifies who is reading and writing,
 * from the user's point of view. It can be one of the following values:
 *
 * | Direction      | Description                                                                       |
 * |----------------|-----------------------------------------------------------------------------------|
 * | IOC_DIR_NONE   | neither userspace nor kernel is writing, ioctl is executed without passing data.  |
 * | IOC_DIR_WRITE  | userspace is writing and kernel is reading.                                       |
 * | IOC_DIR_READ   | kernel is writing and userspace is reading.                                       |
 * | IOC_DIR_RW     | userspace is writing and kernel is writing back into the data structure.          |
 *
 * Returns the result of the ioctl operation; for `IOC_DIR_READ` and
 * `IOC_DIR_RW` this is a string containing the data, otherwise a number as
 * return code.
 *
 * Returns `null` if an error occurred.
 *
 * @function module:io.handle#ioctl
 *
 * @param {number} direction
 * The direction of the ioctl operation. Use constants IOC_DIR_*.
 *
 * @param {number} type
 * The ioctl type (see https://www.kernel.org/doc/html/latest/userspace-api/ioctl/ioctl-number.html)
 *
 * @param {number} num
 * The ioctl sequence number.
 *
 * @param {number|string} [value]
 * The value to pass to the ioctl system call. For `IOC_DIR_NONE`, this argument
 * is ignored. With `IOC_DIR_READ`, the value should be a positive integer
 * specifying the number of bytes to expect from the kernel. For the other
 * directions, `IOC_DIR_WRITE` and `IOC_DIR_RW`, that value parameter must be a
 * string, serving as buffer for the data to send.
 *
 * @returns {?number|?string}
 *
 * @example
 * const handle = io.open('/dev/tty', O_RDWR);
 * const size = handle.ioctl(IOC_DIR_READ, 0x54, 0x13, 8);  // TIOCGWINSZ
 */
static uc_value_t *
uc_io_ioctl(uc_vm_t *vm, size_t nargs)
{
	uc_io_handle_t *handle = uc_fn_thisval("io.handle");
	uc_value_t *direction = uc_fn_arg(0);
	uc_value_t *type = uc_fn_arg(1);
	uc_value_t *num = uc_fn_arg(2);
	uc_value_t *value = uc_fn_arg(3);
	uc_value_t *mem = NULL;
	char *buf = NULL;
	unsigned long req = 0;
	unsigned int dir, ty, nr;
	size_t sz = 0;
	int fd, ret;

	if (!handle || handle->fd < 0)
		err_return(EBADF);

	fd = handle->fd;

	if (ucv_type(direction) != UC_INTEGER || ucv_type(type) != UC_INTEGER ||
	    ucv_type(num) != UC_INTEGER)
		err_return(EINVAL);

	dir = ucv_uint64_get(direction);
	ty = ucv_uint64_get(type);
	nr = ucv_uint64_get(num);

	switch (dir) {
	case IOC_DIR_NONE:
		break;

	case IOC_DIR_WRITE:
		if (ucv_type(value) != UC_STRING)
			err_return(EINVAL);

		sz = ucv_string_length(value);
		buf = ucv_string_get(value);
		break;

	case IOC_DIR_READ:
		if (ucv_type(value) != UC_INTEGER)
			err_return(EINVAL);

		sz = ucv_to_unsigned(value);

		if (errno != 0)
			err_return(errno);

		mem = xalloc(sizeof(uc_string_t) + sz + 1);
		mem->type = UC_STRING;
		mem->refcount = 1;
		buf = ucv_string_get(mem);
		((uc_string_t *)mem)->length = sz;
		break;

	case IOC_DIR_RW:
		if (ucv_type(value) != UC_STRING)
			err_return(EINVAL);

		sz = ucv_string_length(value);
		mem = ucv_string_new_length(ucv_string_get(value), sz);
		buf = ucv_string_get(mem);
		break;

	default:
		err_return(EINVAL);
	}

	req = _IOC(dir, ty, nr, sz);
	ret = ioctl(fd, req, buf);

	if (ret < 0) {
		ucv_put(mem);
		err_return(errno);
	}

	return mem ? mem : ucv_uint64_new(ret);
}

#endif

/**
 * Checks if the file descriptor refers to a terminal.
 *
 * Returns `true` if the descriptor refers to a terminal device.
 *
 * Returns `false` otherwise.
 *
 * Returns `null` if an error occurred.
 *
 * @function module:io.handle#isatty
 *
 * @returns {?boolean}
 *
 * @example
 * const handle = io.new(0);  // stdin
 * if (handle.isatty())
 *     print("Running in a terminal\n");
 */
static uc_value_t *
uc_io_isatty(uc_vm_t *vm, size_t nargs)
{
	uc_io_handle_t *handle;
	int fd;

	handle = uc_fn_thisval("io.handle");

	if (!handle || handle->fd < 0)
		err_return(EBADF);

	fd = handle->fd;

	return ucv_boolean_new(isatty(fd) == 1);
}

/**
 * Closes the file descriptor.
 *
 * Closes the underlying file descriptor. Further operations on this handle
 * will fail.
 *
 * Returns `true` if the descriptor was successfully closed.
 *
 * Returns `null` if an error occurred.
 *
 * @function module:io.handle#close
 *
 * @returns {?boolean}
 *
 * @example
 * const handle = io.open('/tmp/test.txt', O_RDONLY);
 * handle.close();
 */
static uc_value_t *
uc_io_close(uc_vm_t *vm, size_t nargs)
{
	uc_io_handle_t *handle;

	handle = uc_fn_thisval("io.handle");

	if (!handle || handle->fd < 0)
		err_return(EBADF);

	if (close(handle->fd) < 0)
		err_return(errno);

	handle->fd = -1;

	return ucv_boolean_new(true);
}

/**
 * Creates an io.handle from a file descriptor number.
 *
 * Wraps the given file descriptor number in an io.handle object.
 *
 * Returns an io.handle object.
 *
 * Returns `null` if an error occurred.
 *
 * @function module:io#new
 *
 * @param {number} fd
 * The file descriptor number.
 *
 * @returns {?module:io.handle}
 *
 * @example
 * // Wrap stdin
 * const stdin = io.new(0);
 * const data = stdin.read(100);
 */
static uc_value_t *
uc_io_new(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *fdno = uc_fn_arg(0);
	uc_io_handle_t *handle = NULL;
	uc_value_t *res;
	int64_t n;

	if (ucv_type(fdno) != UC_INTEGER)
		err_return(EINVAL);

	n = ucv_int64_get(fdno);

	if (n < 0 || n > INT_MAX)
		err_return(EBADF);

	res = ucv_resource_create_ex(vm, "io.handle",
	                             (void **)&handle, 0, sizeof(*handle));

	if (!handle)
		err_return(ENOMEM);

	handle->fd = (int)n;
	handle->close_on_free = false;  /* Don't own this fd */

	return res;
}

/**
 * Opens a file and returns an io.handle.
 *
 * Opens the specified file with the given flags and mode, returning an
 * io.handle wrapping the resulting file descriptor.
 *
 * Returns an io.handle object.
 *
 * Returns `null` if an error occurred.
 *
 * @function module:io#open
 *
 * @param {string} path
 * The path to the file.
 *
 * @param {number} [flags=O_RDONLY]
 * The open flags (O_RDONLY, O_WRONLY, O_RDWR, etc.).
 *
 * @param {number} [mode=0o666]
 * The file creation mode (used with O_CREAT).
 *
 * @returns {?module:io.handle}
 *
 * @example
 * const handle = io.open('/tmp/test.txt', O_RDWR | O_CREAT, 0o644);
 * handle.write('Hello World\n');
 * handle.close();
 */
static uc_value_t *
uc_io_open(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *path = uc_fn_arg(0);
	uc_value_t *flags = uc_fn_arg(1);
	uc_value_t *mode = uc_fn_arg(2);
	uc_io_handle_t *handle = NULL;
	uc_value_t *res;
	int open_flags = O_RDONLY;
	mode_t open_mode = 0666;
	int fd;

	if (ucv_type(path) != UC_STRING)
		err_return(EINVAL);

	if (flags) {
		if (ucv_type(flags) != UC_INTEGER)
			err_return(EINVAL);

		open_flags = (int)ucv_int64_get(flags);
	}

	if (mode) {
		if (ucv_type(mode) != UC_INTEGER)
			err_return(EINVAL);

		open_mode = (mode_t)ucv_int64_get(mode);
	}

	fd = open(ucv_string_get(path), open_flags, open_mode);

	if (fd < 0)
		err_return(errno);

	res = ucv_resource_create_ex(vm, "io.handle",
	                             (void **)&handle, 0, sizeof(*handle));

	if (!handle)
		err_return(ENOMEM);

	handle->fd = fd;
	handle->close_on_free = true;  /* We own this fd */

	return res;
}

/**
 * Creates a pipe.
 *
 * Creates a unidirectional data channel (pipe) that can be used for
 * inter-process communication. Returns an array containing two io.handle
 * objects: the first is the read end of the pipe, the second is the write end.
 *
 * Data written to the write end can be read from the read end.
 *
 * Returns an array `[read_handle, write_handle]` on success.
 *
 * Returns `null` if an error occurred.
 *
 * @function module:io#pipe
 *
 * @returns {?Array<module:io.handle>}
 *
 * @example
 * const [reader, writer] = io.pipe();
 * writer.write('Hello from pipe!');
 * const data = reader.read(100);
 * print(data, "\n");  // Prints: Hello from pipe!
 */
static uc_value_t *
uc_io_pipe(uc_vm_t *vm, size_t nargs)
{
	uc_io_handle_t *read_handle = NULL, *write_handle = NULL;
	uc_value_t *result, *res;
	int fds[2];

	if (pipe(fds) < 0)
		err_return(errno);

	res = ucv_resource_create_ex(vm, "io.handle", (void **)&read_handle, 0,
	                             sizeof(*read_handle));

	if (!read_handle)
		err_return(ENOMEM);

	read_handle->fd = fds[0];
	read_handle->close_on_free = true;

	result = ucv_array_new(vm);
	ucv_array_push(result, res);

	res = ucv_resource_create_ex(vm, "io.handle", (void **)&write_handle, 0,
	                             sizeof(*write_handle));

	if (!write_handle) {
		ucv_put(result);
		err_return(ENOMEM);
	}

	write_handle->fd = fds[1];
	write_handle->close_on_free = true;

	ucv_array_push(result, res);

	return result;
}

/**
 * Creates an io.handle from various value types.
 *
 * Creates an io.handle by extracting the file descriptor from the given value.
 * The value can be:
 * - An integer file descriptor number
 * - An fs.file, fs.proc, or socket resource
 * - Any object/array/resource with a fileno() method
 *
 * Returns an io.handle object.
 *
 * Returns `null` if an error occurred or the value cannot be converted.
 *
 * @function module:io#from
 *
 * @param {*} value
 * The value to convert.
 *
 * @returns {?module:io.handle}
 *
 * @example
 * import { open as fsopen } from 'fs';
 * const fp = fsopen('/tmp/test.txt', 'r');
 * const handle = io.from(fp);
 * const data = handle.read(100);
 */
static uc_value_t *
uc_io_from(uc_vm_t *vm, size_t nargs)
{
	uc_io_handle_t *handle = NULL;
	uc_value_t *val = uc_fn_arg(0);
	uc_value_t *res;
	int fd;

	if (!val)
		err_return(EINVAL);

	if (!get_fd_from_value(vm, val, &fd))
		return NULL;

	res = ucv_resource_create_ex(vm, "io.handle",
	                             (void **)&handle, 0, sizeof(*handle));

	if (!handle)
		err_return(ENOMEM);

	handle->fd = fd;
	handle->close_on_free = false;  /* Don't own this fd, it's from external source */

	return res;
}

static void
uc_io_handle_free(void *ptr)
{
	uc_io_handle_t *handle = ptr;

	if (!handle)
		return;

	if (handle->close_on_free && handle->fd >= 0)
		close(handle->fd);
}

static const uc_function_list_t io_handle_fns[] = {
	{ "read",		uc_io_read },
	{ "write",		uc_io_write },
	{ "seek",		uc_io_seek },
	{ "tell",		uc_io_tell },
	{ "dup",		uc_io_dup },
	{ "dup2",		uc_io_dup2 },
	{ "fileno",		uc_io_fileno },
	{ "fcntl",		uc_io_fcntl },
#ifdef HAS_IOCTL
	{ "ioctl",		uc_io_ioctl },
#endif
	{ "isatty",		uc_io_isatty },
	{ "close",		uc_io_close },
	{ "error",		uc_io_error },
};

static const uc_function_list_t io_fns[] = {
	{ "error",		uc_io_error },
	{ "new",		uc_io_new },
	{ "open",		uc_io_open },
	{ "from",		uc_io_from },
	{ "pipe",		uc_io_pipe },
};

#define ADD_CONST(x) ucv_object_add(scope, #x, ucv_int64_new(x))

void uc_module_init(uc_vm_t *vm, uc_value_t *scope)
{
	uc_function_list_register(scope, io_fns);

	ADD_CONST(O_RDONLY);
	ADD_CONST(O_WRONLY);
	ADD_CONST(O_RDWR);
	ADD_CONST(O_CREAT);
	ADD_CONST(O_EXCL);
	ADD_CONST(O_TRUNC);
	ADD_CONST(O_APPEND);
	ADD_CONST(O_NONBLOCK);
	ADD_CONST(O_NOCTTY);
	ADD_CONST(O_SYNC);
	ADD_CONST(O_CLOEXEC);
#ifdef O_DIRECTORY
	ADD_CONST(O_DIRECTORY);
#endif
#ifdef O_NOFOLLOW
	ADD_CONST(O_NOFOLLOW);
#endif

	ADD_CONST(SEEK_SET);
	ADD_CONST(SEEK_CUR);
	ADD_CONST(SEEK_END);

	ADD_CONST(F_DUPFD);
#ifdef F_DUPFD_CLOEXEC
	ADD_CONST(F_DUPFD_CLOEXEC);
#endif
	ADD_CONST(F_GETFD);
	ADD_CONST(F_SETFD);
	ADD_CONST(F_GETFL);
	ADD_CONST(F_SETFL);
	ADD_CONST(F_GETLK);
	ADD_CONST(F_SETLK);
	ADD_CONST(F_SETLKW);
	ADD_CONST(F_GETOWN);
	ADD_CONST(F_SETOWN);

	ADD_CONST(FD_CLOEXEC);

#ifdef HAS_IOCTL
	ADD_CONST(IOC_DIR_NONE);
	ADD_CONST(IOC_DIR_READ);
	ADD_CONST(IOC_DIR_WRITE);
	ADD_CONST(IOC_DIR_RW);
#endif

	uc_type_declare(vm, "io.handle", io_handle_fns, uc_io_handle_free);
}