From 38cdd8382fb831db985b643007c87911080582b5 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Tue, 11 Nov 2025 23:24:56 -0500 Subject: [PATCH 1/5] Make an injectable user-space interface for php-wasm instances --- .../src/lib/file-lock-manager-for-node.ts | 2 +- packages/php-wasm/node/src/lib/index.ts | 1 - .../php-wasm/node/src/lib/load-runtime.ts | 2 +- .../universal/src/lib/emscripten-types.ts | 7 +- .../src/lib/file-lock-manager.ts | 0 packages/php-wasm/universal/src/lib/index.ts | 2 + .../universal/src/lib/os-kernel-space.ts | 11 + .../universal/src/lib/os-user-space.ts | 873 ++++++++++++++++++ packages/php-wasm/universal/tsconfig.json | 4 +- 9 files changed, 897 insertions(+), 5 deletions(-) rename packages/php-wasm/{node => universal}/src/lib/file-lock-manager.ts (100%) create mode 100644 packages/php-wasm/universal/src/lib/os-kernel-space.ts create mode 100644 packages/php-wasm/universal/src/lib/os-user-space.ts diff --git a/packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts b/packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts index 0a740dba59..a242b0d51a 100644 --- a/packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts +++ b/packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts @@ -13,7 +13,7 @@ import type { WholeFileLockOp, Pid, Fd, -} from './file-lock-manager'; +} from '@php-wasm/universal'; type LockMode = 'exclusive' | 'shared' | 'unlock'; diff --git a/packages/php-wasm/node/src/lib/index.ts b/packages/php-wasm/node/src/lib/index.ts index e27461ff33..0cc3d1ed89 100644 --- a/packages/php-wasm/node/src/lib/index.ts +++ b/packages/php-wasm/node/src/lib/index.ts @@ -3,6 +3,5 @@ export * from './networking/with-networking'; export * from './load-runtime'; export * from './use-host-filesystem'; export * from './node-fs-mount'; -export * from './file-lock-manager'; export * from './file-lock-manager-for-node'; export * from './xdebug/with-xdebug'; diff --git a/packages/php-wasm/node/src/lib/load-runtime.ts b/packages/php-wasm/node/src/lib/load-runtime.ts index 0b49ffba38..80ed303026 100644 --- a/packages/php-wasm/node/src/lib/load-runtime.ts +++ b/packages/php-wasm/node/src/lib/load-runtime.ts @@ -3,12 +3,12 @@ import type { EmscriptenOptions, PHPRuntime, RemoteAPI, + FileLockManager, } from '@php-wasm/universal'; import { loadPHPRuntime, FSHelpers } from '@php-wasm/universal'; import fs from 'fs'; import { getPHPLoaderModule } from '.'; import { withNetworking } from './networking/with-networking'; -import type { FileLockManager } from './file-lock-manager'; import { withXdebug, type XdebugOptions } from './xdebug/with-xdebug'; import { withIntl } from './extensions/intl/with-intl'; import { joinPaths } from '@php-wasm/util'; diff --git a/packages/php-wasm/universal/src/lib/emscripten-types.ts b/packages/php-wasm/universal/src/lib/emscripten-types.ts index e149cdab8e..d833ee7c18 100644 --- a/packages/php-wasm/universal/src/lib/emscripten-types.ts +++ b/packages/php-wasm/universal/src/lib/emscripten-types.ts @@ -149,7 +149,7 @@ export namespace Emscripten { export interface Mount { type: Emscripten.FileSystemType; - opts: object; + opts: Record; mountpoint: string; mounts: Mount[]; root: FSNode; @@ -185,6 +185,10 @@ export namespace Emscripten { write: boolean; readonly isFolder: boolean; readonly isDevice: boolean; + + // NOTE: As of 2025-11-11, this property is added by a php-wasm patch + // for NODEFS.createNode(). It is not part of the Emscripten FSNode interface. + readonly isSharedFS?: boolean; } export interface ErrnoError extends Error { @@ -394,6 +398,7 @@ export namespace Emscripten { export declare const MEMFS: Emscripten.FileSystemType; export declare const NODEFS: Emscripten.FileSystemType; export declare const IDBFS: Emscripten.FileSystemType; + export declare const PROXYFS: Emscripten.FileSystemType; // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html type StringToType = R extends Emscripten.JSType diff --git a/packages/php-wasm/node/src/lib/file-lock-manager.ts b/packages/php-wasm/universal/src/lib/file-lock-manager.ts similarity index 100% rename from packages/php-wasm/node/src/lib/file-lock-manager.ts rename to packages/php-wasm/universal/src/lib/file-lock-manager.ts diff --git a/packages/php-wasm/universal/src/lib/index.ts b/packages/php-wasm/universal/src/lib/index.ts index fe0b27ff8d..b09b45b964 100644 --- a/packages/php-wasm/universal/src/lib/index.ts +++ b/packages/php-wasm/universal/src/lib/index.ts @@ -88,3 +88,5 @@ export { sandboxedSpawnHandlerFactory } from './sandboxed-spawn-handler-factory' export * from './api'; export type { WithAPIState as WithIsReady } from './api'; + +export type * from './file-lock-manager'; diff --git a/packages/php-wasm/universal/src/lib/os-kernel-space.ts b/packages/php-wasm/universal/src/lib/os-kernel-space.ts new file mode 100644 index 0000000000..65c231ef06 --- /dev/null +++ b/packages/php-wasm/universal/src/lib/os-kernel-space.ts @@ -0,0 +1,11 @@ +// TODO: Consider merging FileLockManager file with os-kernel-space. +import type { FileLockManager } from './file-lock-manager'; + +// TODO: Consider merging FileLockManager into this type. +export class OSKernelSpace { + readonly fileLockManager: FileLockManager; + + constructor(fileLockManager: FileLockManager) { + this.fileLockManager = fileLockManager; + } +} diff --git a/packages/php-wasm/universal/src/lib/os-user-space.ts b/packages/php-wasm/universal/src/lib/os-user-space.ts new file mode 100644 index 0000000000..8cc55a3576 --- /dev/null +++ b/packages/php-wasm/universal/src/lib/os-user-space.ts @@ -0,0 +1,873 @@ +// TODO: Move file manager into kernel space. +// TODO: Move FileLockManager into php-wasm/universal to resolve this +import type { + RequestedRangeLock, + WholeFileLock, + WholeFileLockOp, +} from './file-lock-manager'; +import type { Emscripten } from './emscripten-types'; +import type { OSKernelSpace } from './os-kernel-space'; + +type FSNode = Emscripten.FS.FSNode; + +type NonZeroNumber = Exclude; +type ResultTuple = + | [value: T, errorCode: 0] + | [value: never, errorCode: NonZeroNumber]; + +// TODO: Consider better name than OSUserSpace. Maybe WasmUserSpace, SystemUserSpace, etc? +export type OSUserSpaceContext = { + pid: number; + // TODO: When receiving this context, validate that all these fields exist. + constants: { + F_RDLCK: number; + F_WRLCK: number; + F_UNLCK: number; + F_GETFL: number; + O_ACCMODE: number; + O_RDONLY: number; + O_WRONLY: number; + O_APPEND: number; + O_NONBLOCK: number; + F_SETFL: number; + F_GETLK: number; + F_SETLK: number; + F_SETLKW: number; + SEEK_SET: number; + SEEK_CUR: number; + SEEK_END: number; + // TODO: Move these values to ES prefix or someplace like that. + // Emscripten does not expose these constants to JS, so we hardcode them here. + // Based on + // https://github.com/emscripten-core/emscripten/blob/76860cc47cef67f5712a7a03a247bc1baabf7ba4/system/lib/libc/musl/include/sys/file.h#L7-L10 + LOCK_SH: 1; + LOCK_EX: 2; + LOCK_NB: 4; + LOCK_UN: 8; + }; + errnoCodes: { + EBADF: NonZeroNumber; + EINVAL: NonZeroNumber; + EAGAIN: NonZeroNumber; + EDEADLK: NonZeroNumber; + EWOULDBLOCK: NonZeroNumber; + }; + memory: { + HEAP8: Int8Array; + HEAPU8: Uint8Array; + HEAP16: Int16Array; + HEAPU16: Uint16Array; + HEAP32: Int32Array; + HEAPU32: Uint32Array; + HEAPF32: Float32Array; + HEAP64: BigInt64Array; + HEAPU64: BigUint64Array; + HEAPF64: Float64Array; + }; + builtins: { + fd_close: (fd: number) => number; + fcntl64: (fd: number, cmd: number, varargs?: any) => number; + getStreamFromFD: (fd: number) => Emscripten.FS.FSStream; + }; + helpers: { + get_end_offset_for_fd: (fd: number) => bigint; + }; + FS: typeof Emscripten.FS; + PROXYFS: typeof Emscripten.PROXYFS & { + realPath(node: FSNode): string; + }; + NODEFS: typeof Emscripten.NODEFS & { + realPath(node: FSNode): string; + }; + // TODO: Likely rename this. There's no reason it should be different from the rest of the names. + _js_wasm_trace: (...args: any[]) => void; +}; + +export function bindUserSpace( + { fileLockManager }: OSKernelSpace, + { + pid, + constants: { + F_RDLCK, + F_WRLCK, + F_UNLCK, + F_GETFL, + O_ACCMODE, + O_RDONLY, + O_WRONLY, + O_APPEND, + O_NONBLOCK, + F_SETFL, + F_GETLK, + F_SETLK, + F_SETLKW, + SEEK_SET, + SEEK_CUR, + SEEK_END, + LOCK_SH, + LOCK_EX, + LOCK_NB, + LOCK_UN, + }, + errnoCodes: { EBADF, EINVAL, EAGAIN, EDEADLK, EWOULDBLOCK }, + memory: { HEAP16, HEAP64, HEAP32 }, + builtins, + helpers, + FS, + PROXYFS, + NODEFS, + _js_wasm_trace, + }: OSUserSpaceContext +) { + class VarArgsAccessor { + argsAddr: number; + + constructor(argsAddr: number) { + this.argsAddr = argsAddr; + } + + getNextAsPointer(): number { + return this.getNextAsInt(); + } + + getNextAsInt(): number { + const value = HEAP32[this.argsAddr]; + this.argsAddr += 4; + return value; + } + } + + type FcntlLockState = typeof F_RDLCK | typeof F_WRLCK | typeof F_UNLCK; + const locking = { + /* + * This is a set of possibly locked file descriptors. + * + * When a file descriptor is closed, we need to release any associated held by this process. + * Instead of trying remember and forget file descriptors as they are locked and unlocked, + * we just track file descriptors we have locked before and try an unlock when they are closed. + */ + maybeLockedFds: new Set(), + + // TODO: Move this comment whereever these values are passed to this binding function. + // From: + // https://github.com/emscripten-core/emscripten/blob/66d2137b0381ac35f7e2346b2d6a90abd0f1211a/system/lib/libc/musl/include/fcntl.h#L58-L60 + // F_RDLCK: 0, + // F_WRLCK: 1, + // F_UNLCK: 2, + + lockStateToFcntl: { + shared: F_RDLCK, + exclusive: F_WRLCK, + unlocked: F_UNLCK, + } as const satisfies Record, + fcntlToLockState: { + [F_RDLCK as FcntlLockState]: 'shared', + [F_WRLCK as FcntlLockState]: 'exclusive', + [F_UNLCK as FcntlLockState]: 'unlocked', + } as const satisfies Record, + is_path_to_shared_fs(path: string) { + _js_wasm_trace('is_path_to_shared_fs(%s)', path); + const { node } = FS.lookupPath(path, { noent_okay: true }); + if (node.mount.type !== PROXYFS) { + return !!node.isSharedFS; + } + + // TODO: Do we still need to support PROXYFS now that Playground CLI uses NODEFS everywhere? + // This looks like a PROXYFS node. Let's try a lookup. + const nodePath = PROXYFS.realPath(node); + const backingFs = node?.mount?.opts?.fs; + if (backingFs) { + // Tolerate ENOENT because looking up a MEMFS node by path always fails. + const { node: backingNode } = backingFs.lookupPath(nodePath, { + noent_okay: true, + }); + return !!backingNode?.isSharedFS; + } + + return false; + }, + get_fd_access_mode(fd: number) { + return builtins.fcntl64(fd, F_GETFL) & O_ACCMODE; + }, + get_vfs_path_from_fd(fd: number): ResultTuple { + try { + return [FS.readlink(`/proc/self/fd/${fd}`), 0]; + } catch { + return [null as never, EBADF] as const; + } + }, + + get_native_path_from_vfs_path(vfsPath: string) { + const { node } = FS.lookupPath(vfsPath, { + noent_okay: true, + }); + if (!node) { + throw new Error(`No node found for VFS path ${vfsPath}`); + } + if (node.mount.type === NODEFS) { + return NODEFS.realPath(node); + } else if (node.mount.type === PROXYFS) { + // TODO: Tolerate ENOENT here? + const { node: backingNode, path: backingPath } = + node.mount.opts.fs.lookupPath(vfsPath); + _js_wasm_trace( + 'backingNode for %s: %s', + vfsPath, + backingPath, + backingNode + ); + return backingNode.mount.type.realPath(backingNode); + } else { + throw new Error( + `Unsupported filesystem type for path ${vfsPath}` + ); + } + }, + + check_lock_params(fd: number, l_type: number) { + const accessMode = locking.get_fd_access_mode(fd); + if ( + (l_type === F_WRLCK && accessMode === O_RDONLY) || + (l_type === F_RDLCK && accessMode === O_WRONLY) + ) { + return EBADF; + } + + return 0; + }, + }; + + type FlockStruct = { + l_type: number; + l_whence: number; + l_start: bigint; + l_len: bigint; + l_pid: number; + }; + + // NOTE: With the exception of l_type, these offsets are not exposed to + // JS by Emscripten, so we hardcode them here. + const emscripten_flock_l_type_offset = 0; + const emscripten_flock_l_whence_offset = 2; + const emscripten_flock_l_start_offset = 8; + const emscripten_flock_l_len_offset = 16; + const emscripten_flock_l_pid_offset = 24; + + /** + * Read the flock struct at the given address. + * + * @param {bigint} flockStructAddress - the address of the flock struct + * @returns the flock struct + */ + // TODO: Does this arg type need to be a bigint? + function read_flock_struct(flockStructAddress: number) { + /* + * NOTE: Since we are using HEAP vars like HEAP16 and HEAP64, + * we need to adjust offsets to address the word size of each HEAP. + * + * For example, an offset of 64 bytes is the following for each HEAP: + * - HEAP8: 64 (the 64th byte) + * - HEAP16: 32 (the 32nd 16-bit word) + * - HEAP32: 16 (the 16th 32-bit word) + * - HEAP64: 8 (the 8th 64-bit word) + * + * We get a word offset by dividing the byte offset by the word size. + */ + return { + l_type: HEAP16[ + // Shift right by 1 to divide by 2^1. + (flockStructAddress + emscripten_flock_l_type_offset) >> 1 + ], + l_whence: + HEAP16[ + // Shift right by 1 to divide by 2^1. + (flockStructAddress + emscripten_flock_l_whence_offset) >> 1 + ], + l_start: + HEAP64[ + // Shift right by 3 to divide by 2^3. + (flockStructAddress + emscripten_flock_l_start_offset) >> 3 + ], + l_len: HEAP64[ + // Shift right by 3 to divide by 2^3. + (flockStructAddress + emscripten_flock_l_len_offset) >> 3 + ], + l_pid: HEAP32[ + // Shift right by 2 to divide by 2^2. + (flockStructAddress + emscripten_flock_l_pid_offset) >> 2 + ], + }; + } + + /** + * Update the flock struct at the given address with the given fields. + * + * @param {bigint} flockStructAddress - the address of the flock struct + * @param {object} fields - the fields to update + */ + function update_flock_struct( + flockStructAddress: number, + fields: Partial + ) { + /* + * NOTE: Since we are using HEAP vars like HEAP16 and HEAP64, + * we need to adjust offsets to address the word size of each HEAP. + * + * For example, an offset of 64 bytes is the following for each HEAP: + * - HEAP8: 64 (the 64th byte) + * - HEAP16: 32 (the 32nd 16-bit word) + * - HEAP32: 16 (the 16th 32-bit word) + * - HEAP64: 8 (the 8th 64-bit word) + * + * We get a word offset by dividing the byte offset by the word size. + */ + if (fields.l_type !== undefined) { + HEAP16[ + // Shift right by 1 to divide by 2^1. + (flockStructAddress + emscripten_flock_l_type_offset) >> 1 + ] = fields.l_type; + } + if (fields.l_whence !== undefined) { + HEAP16[ + // Shift right by 1 to divide by 2^1. + (flockStructAddress + emscripten_flock_l_whence_offset) >> 1 + ] = fields.l_whence; + } + if (fields.l_start !== undefined) { + HEAP64[ + // Shift right by 3 to divide by 2^3. + (flockStructAddress + emscripten_flock_l_start_offset) >> 3 + ] = fields.l_start; + } + if (fields.l_len !== undefined) { + HEAP64[ + // Shift right by 3 to divide by 2^3. + (flockStructAddress + emscripten_flock_l_len_offset) >> 3 + ] = fields.l_len; + } + if (fields.l_pid !== undefined) { + HEAP32[ + // Shift right by 2 to divide by 2^2. + (flockStructAddress + emscripten_flock_l_pid_offset) >> 2 + ] = fields.l_pid; + } + } + + /** + * Resolve the base address of the range depending on the whence and start offset. + * + * @param {number} fd - the file descriptor + * @param {number} whence - what the start offset is relative to + * @param {bigint} startOffset - the offset from the whence + * @returns The resolved offset and the errno. If there is an error, + * the resolved offset is null, and the errno is non-zero. + */ + function get_base_address(fd: number, whence: number, startOffset: bigint) { + let baseAddress; + switch (whence) { + case SEEK_SET: + baseAddress = 0n; + break; + case SEEK_CUR: + try { + const stream = builtins.getStreamFromFD(fd); + baseAddress = FS.llseek(stream, 0, whence); + } catch (e) { + _js_wasm_trace( + 'get_base_address(%d, %d, %d) getStreamFromFD error %s', + fd, + whence, + startOffset, + e + ); + return [null, EINVAL]; + } + break; + case SEEK_END: + baseAddress = helpers.get_end_offset_for_fd(fd); + break; + default: + return [null, EINVAL]; + } + + if (baseAddress == -1) { + // We cannot resolve the offset within the file. + // Let's treat this as a problem with the file descriptor. + return [null, EBADF]; + } + + const resolvedOffset = baseAddress + startOffset; + if (resolvedOffset < 0) { + // This is not a valid offset. Report args as invalid. + return [null, EINVAL]; + } + + return [resolvedOffset, 0]; + } + + // TODO: Should command just be a string representation of const name? + async function fcntl64(fd: number, cmd: number, varargs?: number) { + switch (cmd) { + case F_GETLK: { + _js_wasm_trace('fcntl(%d, F_GETLK)', fd); + const [vfsPath, vfsPathErrno] = + locking.get_vfs_path_from_fd(fd); + if (vfsPathErrno !== 0) { + _js_wasm_trace( + 'fcntl(%d, F_GETLK) %s get_vfs_path_from_fd errno %d', + fd, + vfsPath, + vfsPathErrno + ); + return -EBADF; + } + + const varArgsAccessor = new VarArgsAccessor(varargs!); + const flockStructAddr = varArgsAccessor.getNextAsPointer(); + + if (!locking.is_path_to_shared_fs(vfsPath)) { + _js_wasm_trace( + "fcntl(%d, F_GETLK) locking is not implemented for non-NodeFS path '%s'", + fd, + vfsPath + ); + + // If not a NodeFS path, we can't lock it. + // Default to succeeding as Emscripten does. + update_flock_struct(flockStructAddr, { + l_type: F_UNLCK, + }); + return 0; + } + + const flockStruct = read_flock_struct(flockStructAddr); + + if (!(flockStruct.l_type in locking.fcntlToLockState)) { + return -EINVAL; + } + + const paramsCheckErrno = locking.check_lock_params( + fd, + flockStruct.l_type + ); + if (paramsCheckErrno !== 0) { + _js_wasm_trace( + 'fcntl(%d, F_GETLK) %s check_lock_params errno %d', + fd, + vfsPath, + paramsCheckErrno + ); + return -EINVAL; + } + + const requestedLockType = + locking.fcntlToLockState[flockStruct.l_type]; + const [absoluteStartOffset, baseAddressErrno] = + get_base_address( + fd, + flockStruct.l_whence, + flockStruct.l_start + ); + if (baseAddressErrno !== 0) { + _js_wasm_trace( + 'fcntl(%d, F_GETLK) %s get_base_address errno %d', + fd, + vfsPath, + baseAddressErrno + ); + return -EINVAL; + } + + try { + const nativeFilePath = + locking.get_native_path_from_vfs_path(vfsPath); + const conflictingLock = + fileLockManager.findFirstConflictingByteRangeLock( + nativeFilePath, + { + type: requestedLockType, + start: absoluteStartOffset, + end: absoluteStartOffset + flockStruct.l_len, + pid, + } + ); + if (conflictingLock === undefined) { + _js_wasm_trace( + 'fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock type=unlocked start=0x%x end=0x%x', + fd, + vfsPath, + absoluteStartOffset, + absoluteStartOffset + flockStruct.l_len + ); + + update_flock_struct(flockStructAddr, { + l_type: F_UNLCK, + }); + return 0; + } + + _js_wasm_trace( + 'fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock type=%s start=0x%x end=0x%x conflictingLock %d', + fd, + vfsPath, + conflictingLock.type, + conflictingLock.start, + conflictingLock.end, + conflictingLock.pid + ); + + const fcntlLockState = + locking.lockStateToFcntl[conflictingLock.type]; + update_flock_struct(flockStructAddr, { + l_type: fcntlLockState, + l_whence: SEEK_SET, + l_start: conflictingLock.start, + l_len: BigInt( + conflictingLock.end - conflictingLock.start + ), + l_pid: conflictingLock.pid, + }); + return 0; + } catch (e) { + _js_wasm_trace( + 'fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock error %s', + fd, + vfsPath, + e + ); + return -EINVAL; + } + } + case F_SETLK: { + _js_wasm_trace('fcntl(%d, F_SETLK)', fd); + const [vfsPath, vfsPathErrno] = + locking.get_vfs_path_from_fd(fd); + if (vfsPathErrno !== 0) { + _js_wasm_trace( + 'fcntl(%d, F_SETLK) %s get_vfs_path_from_fd errno %d', + fd, + vfsPath, + vfsPathErrno + ); + return -vfsPathErrno; + } + + if (!locking.is_path_to_shared_fs(vfsPath)) { + _js_wasm_trace( + 'fcntl(%d, F_SETLK) locking is not implemented for non-NodeFS path %s', + fd, + vfsPath + ); + + // If not a NodeFS path, we can't lock it. + // Default to succeeding as Emscripten does. + return 0; + } + + const varArgsAccessor = new VarArgsAccessor(varargs!); + const flockStructAddr = varArgsAccessor.getNextAsPointer(); + const flockStruct = read_flock_struct(flockStructAddr); + + const [absoluteStartOffset, baseAddressErrno] = + get_base_address( + fd, + flockStruct.l_whence, + flockStruct.l_start + ); + if (baseAddressErrno !== 0) { + _js_wasm_trace( + 'fcntl(%d, F_SETLK) %s get_base_address errno %d', + fd, + vfsPath, + baseAddressErrno + ); + return -EINVAL; + } + + if (!(flockStruct.l_type in locking.fcntlToLockState)) { + _js_wasm_trace( + 'fcntl(%d, F_SETLK) %s invalid lock type %d', + fd, + vfsPath, + flockStruct.l_type + ); + return -EINVAL; + } + + const paramsCheckErrno = locking.check_lock_params( + fd, + flockStruct.l_type + ); + if (paramsCheckErrno !== 0) { + _js_wasm_trace( + 'fcntl(%d, F_SETLK) %s check_lock_params errno %d', + fd, + vfsPath, + paramsCheckErrno + ); + return -paramsCheckErrno; + } + + locking.maybeLockedFds.add(fd); + + const requestedLockType = + locking.fcntlToLockState[flockStruct.l_type]; + const rangeLock: RequestedRangeLock = { + type: requestedLockType, + start: absoluteStartOffset, + end: absoluteStartOffset + flockStruct.l_len, + pid, + }; + + try { + const nativeFilePath = + locking.get_native_path_from_vfs_path(vfsPath); + _js_wasm_trace( + 'fcntl(%d, F_SETLK) %s calling lockFileByteRange for range lock %s', + fd, + vfsPath, + rangeLock + ); + + const succeeded = fileLockManager.lockFileByteRange( + nativeFilePath, + rangeLock + ); + + _js_wasm_trace( + 'fcntl(%d, F_SETLK) %s lockFileByteRange returned %d for range lock %s', + fd, + vfsPath, + succeeded, + rangeLock + ); + return succeeded ? 0 : -EAGAIN; + } catch (e) { + _js_wasm_trace( + 'fcntl(%d, F_SETLK) %s lockFileByteRange error %s for range lock %s', + fd, + vfsPath, + e, + rangeLock + ); + return -EINVAL; + } + } + // @TODO: Implement waiting for lock + case F_SETLKW: { + // We do not yet support the blocking form of flock(). + // We respond with EDEADLK to indicate failure + // because it is a known errno for a failed F_SETLKW command. + return -EDEADLK; + } + case F_SETFL: { + /** + * Overrides the core Emscripten implementation to reflect what + * fcntl does in linux kernel. This implementation is still missing + * a bunch of nuance, but, unlike the core Emscripten implementation, + * it overrides the stream flags while preserving non-stream flags. + * + * @see fcntl.c: + * https://github.com/torvalds/linux/blob/a79a588fc1761dc12a3064fc2f648ae66cea3c5a/fs/fcntl.c#L39 + */ + let arg = 0; + if (varargs !== undefined) { + const varArgsAccessor = new VarArgsAccessor(varargs); + arg = varArgsAccessor.getNextAsInt(); + } + + const stream = builtins.getStreamFromFD(fd); + + // Update the stream flags + const SETFL_MASK = O_APPEND | O_NONBLOCK; + stream.flags = + (arg & SETFL_MASK) | (stream.flags & ~SETFL_MASK); + + return 0; + } + default: + return builtins.fcntl64(fd, cmd, varargs); + } + } + + async function flock(fd: number, op: number) { + _js_wasm_trace('js_flock(%d, %d)', fd, op); + + type FlockOp = typeof LOCK_SH | typeof LOCK_EX | typeof LOCK_UN; + const flockToLockOpType = { + [LOCK_SH]: 'shared', + [LOCK_EX]: 'exclusive', + [LOCK_UN]: 'unlock', + } as const satisfies Record; + + const [vfsPath, vfsPathErrno] = locking.get_vfs_path_from_fd(fd); + if (vfsPathErrno !== 0) { + _js_wasm_trace( + 'js_flock(%d, %d) get_vfs_path_from_fd errno %d', + fd, + op, + vfsPath, + vfsPathErrno + ); + return -vfsPathErrno; + } + + if (!locking.is_path_to_shared_fs(vfsPath)) { + _js_wasm_trace( + 'flock(%d, %d) locking is not implemented for non-NodeFS path %s', + fd, + op, + vfsPath + ); + // If not a NodeFS path, we can't lock it. + // Default to succeeding as Emscripten does. + return 0; + } + + const paramsCheckErrno = locking.check_lock_params(fd, op); + if (paramsCheckErrno !== 0) { + _js_wasm_trace( + 'js_flock(%d, %d) check_lock_params errno %d', + fd, + op, + paramsCheckErrno + ); + return -paramsCheckErrno; + } + + // @TODO: Consider supporting blocking mode of flock() + if ((op & LOCK_NB) === 0) { + _js_wasm_trace( + 'js_flock(%d, %d) blocking mode of flock() is not implemented', + fd, + op + ); + // We do not yet support the blocking form of flock(). + // We respond with EINVAL to indicate failure + // because it is a known errno for a failed blocking flock(). + return -EINVAL; + } + + const maskedOp = op & ((LOCK_SH | LOCK_EX | LOCK_UN) as FlockOp | 0); + + if (maskedOp === 0) { + _js_wasm_trace( + 'js_flock(%d, %d) invalid flock() operation', + fd, + op + ); + return -EINVAL; + } + + const lockOpType = flockToLockOpType[maskedOp as FlockOp]; + if (lockOpType === undefined) { + _js_wasm_trace( + 'js_flock(%d, %d) invalid flock() operation', + fd, + op + ); + return -EINVAL; + } + + try { + const nativeFilePath = + locking.get_native_path_from_vfs_path(vfsPath); + const obtainedLock = await fileLockManager.lockWholeFile( + nativeFilePath, + { + type: lockOpType, + pid: pid, + fd, + } + ); + _js_wasm_trace( + 'js_flock(%d, %d) lockWholeFile %s returned %d', + fd, + op, + vfsPath, + obtainedLock + ); + return obtainedLock ? 0 : -EWOULDBLOCK; + } catch (e) { + _js_wasm_trace( + 'js_flock(%d, %d) lockWholeFile error %s', + fd, + op, + e + ); + return -EINVAL; + } + } + + async function fd_close(fd: number) { + // We have to get the VFS path from the file descriptor + // before closing it. + const [vfsPath, vfsPathResolutionErrno] = + locking.get_vfs_path_from_fd(fd); + + const fdCloseResult = builtins.fd_close(fd); + if (fdCloseResult !== 0 || !locking.maybeLockedFds.has(fd)) { + _js_wasm_trace('fd_close(%d) result %d', fd, fdCloseResult); + return fdCloseResult; + } + + if (vfsPathResolutionErrno !== 0) { + _js_wasm_trace( + 'fd_close(%d) get_vfs_path_from_fd error %d', + fd, + vfsPathResolutionErrno + ); + /* + * It looks like the file may have had an associated lock, + * but since we cannot look up the path, + * there is nothing more for us to do. + * + * NOTE: This seems possible for files that are locked and + * then unlinked before close. It is an opportunity for a + * lock to be orphaned in the lock manager. + * @TODO: Explore how to ensure cleanup in this case. + */ + return fdCloseResult; + } + + try { + const nativeFilePath = + locking.get_native_path_from_vfs_path(vfsPath); + await fileLockManager.releaseLocksForProcessFd( + pid, + fd, + nativeFilePath + ); + _js_wasm_trace('fd_close(%d) release locks success', fd); + } catch (e) { + _js_wasm_trace("fd_close(%d) error '%s'", fd, e); + } finally { + locking.maybeLockedFds.delete(fd); + } + return fdCloseResult; + } + + // TODO: Implement based on current process + // TODO: Replace with process exit handler + async function js_release_file_locks() { + _js_wasm_trace('js_release_file_locks()'); + if (!pid || !fileLockManager) { + _js_wasm_trace('js_release_file_locks no pid or file lock manager'); + return; + } + + try { + await fileLockManager.releaseLocksForProcess(pid); + _js_wasm_trace('js_release_file_locks succeeded'); + } catch (e) { + _js_wasm_trace('js_release_file_locks error %s', e); + } + } + + return { + fcntl64, + flock, + fd_close, + js_release_file_locks, + }; +} diff --git a/packages/php-wasm/universal/tsconfig.json b/packages/php-wasm/universal/tsconfig.json index fe794238d4..18d7a4b681 100644 --- a/packages/php-wasm/universal/tsconfig.json +++ b/packages/php-wasm/universal/tsconfig.json @@ -4,7 +4,9 @@ "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, + // "noPropertyAccessFromIndexSignature": true, + // TODO: Why do we want to avoid property access from index signature? + "noPropertyAccessFromIndexSignature": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "types": ["vitest", "vite/client"] From 60554a93ce58acfc7bfd5158ebbe19e3f961c0a8 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Thu, 13 Nov 2025 21:45:47 -0500 Subject: [PATCH 2/5] Move to user space params that require less php-wasm recompilation --- .../universal/src/lib/os-user-space.ts | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/php-wasm/universal/src/lib/os-user-space.ts b/packages/php-wasm/universal/src/lib/os-user-space.ts index 8cc55a3576..18071fdea0 100644 --- a/packages/php-wasm/universal/src/lib/os-user-space.ts +++ b/packages/php-wasm/universal/src/lib/os-user-space.ts @@ -64,19 +64,23 @@ export type OSUserSpaceContext = { HEAPU64: BigUint64Array; HEAPF64: Float64Array; }; - builtins: { - fd_close: (fd: number) => number; - fcntl64: (fd: number, cmd: number, varargs?: any) => number; - getStreamFromFD: (fd: number) => Emscripten.FS.FSStream; + wasmImports: { + builtin_fcntl64: (fd: number, cmd: number, varargs?: any) => number; + builtin_fd_close: (fd: number) => number; + }; + wasmExports: { + wasm_get_end_offset: (fd: number) => bigint; }; - helpers: { - get_end_offset_for_fd: (fd: number) => bigint; + syscalls: { + getStreamFromFD: (fd: number) => Emscripten.FS.FSStream; }; FS: typeof Emscripten.FS; PROXYFS: typeof Emscripten.PROXYFS & { + // TODO: Add this method to our main Emscripten FS types realPath(node: FSNode): string; }; NODEFS: typeof Emscripten.NODEFS & { + // TODO: Add this method to our main Emscripten FS types realPath(node: FSNode): string; }; // TODO: Likely rename this. There's no reason it should be different from the rest of the names. @@ -87,6 +91,7 @@ export function bindUserSpace( { fileLockManager }: OSKernelSpace, { pid, + memory: { HEAP16, HEAP64, HEAP32 }, constants: { F_RDLCK, F_WRLCK, @@ -110,9 +115,9 @@ export function bindUserSpace( LOCK_UN, }, errnoCodes: { EBADF, EINVAL, EAGAIN, EDEADLK, EWOULDBLOCK }, - memory: { HEAP16, HEAP64, HEAP32 }, - builtins, - helpers, + wasmImports: { builtin_fcntl64, builtin_fd_close }, + wasmExports: { wasm_get_end_offset }, + syscalls: { getStreamFromFD }, FS, PROXYFS, NODEFS, @@ -187,7 +192,7 @@ export function bindUserSpace( return false; }, get_fd_access_mode(fd: number) { - return builtins.fcntl64(fd, F_GETFL) & O_ACCMODE; + return builtin_fcntl64(fd, F_GETFL) & O_ACCMODE; }, get_vfs_path_from_fd(fd: number): ResultTuple { try { @@ -370,7 +375,7 @@ export function bindUserSpace( break; case SEEK_CUR: try { - const stream = builtins.getStreamFromFD(fd); + const stream = getStreamFromFD(fd); baseAddress = FS.llseek(stream, 0, whence); } catch (e) { _js_wasm_trace( @@ -384,7 +389,7 @@ export function bindUserSpace( } break; case SEEK_END: - baseAddress = helpers.get_end_offset_for_fd(fd); + baseAddress = wasm_get_end_offset(fd); break; default: return [null, EINVAL]; @@ -676,7 +681,7 @@ export function bindUserSpace( arg = varArgsAccessor.getNextAsInt(); } - const stream = builtins.getStreamFromFD(fd); + const stream = getStreamFromFD(fd); // Update the stream flags const SETFL_MASK = O_APPEND | O_NONBLOCK; @@ -686,7 +691,7 @@ export function bindUserSpace( return 0; } default: - return builtins.fcntl64(fd, cmd, varargs); + return builtin_fcntl64(fd, cmd, varargs); } } @@ -805,7 +810,7 @@ export function bindUserSpace( const [vfsPath, vfsPathResolutionErrno] = locking.get_vfs_path_from_fd(fd); - const fdCloseResult = builtins.fd_close(fd); + const fdCloseResult = builtin_fd_close(fd); if (fdCloseResult !== 0 || !locking.maybeLockedFds.has(fd)) { _js_wasm_trace('fd_close(%d) result %d', fd, fdCloseResult); return fdCloseResult; From 155fd6970ef186fe48f61c5a0e2f38af5f14a724 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Thu, 13 Nov 2025 21:55:43 -0500 Subject: [PATCH 3/5] Remove stale, commented code --- packages/php-wasm/universal/src/lib/os-user-space.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/php-wasm/universal/src/lib/os-user-space.ts b/packages/php-wasm/universal/src/lib/os-user-space.ts index 18071fdea0..d70d416ee0 100644 --- a/packages/php-wasm/universal/src/lib/os-user-space.ts +++ b/packages/php-wasm/universal/src/lib/os-user-space.ts @@ -153,13 +153,6 @@ export function bindUserSpace( */ maybeLockedFds: new Set(), - // TODO: Move this comment whereever these values are passed to this binding function. - // From: - // https://github.com/emscripten-core/emscripten/blob/66d2137b0381ac35f7e2346b2d6a90abd0f1211a/system/lib/libc/musl/include/fcntl.h#L58-L60 - // F_RDLCK: 0, - // F_WRLCK: 1, - // F_UNLCK: 2, - lockStateToFcntl: { shared: F_RDLCK, exclusive: F_WRLCK, From b9ec6b94f90ba70b5f1e6d07b656fe7a3d4933bf Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Thu, 13 Nov 2025 21:58:42 -0500 Subject: [PATCH 4/5] Move to imported js_wasm_trace --- .../universal/src/lib/os-user-space.ts | 93 ++++++++----------- 1 file changed, 39 insertions(+), 54 deletions(-) diff --git a/packages/php-wasm/universal/src/lib/os-user-space.ts b/packages/php-wasm/universal/src/lib/os-user-space.ts index d70d416ee0..256f9719b7 100644 --- a/packages/php-wasm/universal/src/lib/os-user-space.ts +++ b/packages/php-wasm/universal/src/lib/os-user-space.ts @@ -67,6 +67,7 @@ export type OSUserSpaceContext = { wasmImports: { builtin_fcntl64: (fd: number, cmd: number, varargs?: any) => number; builtin_fd_close: (fd: number) => number; + js_wasm_trace: (...args: any[]) => void; }; wasmExports: { wasm_get_end_offset: (fd: number) => bigint; @@ -83,8 +84,6 @@ export type OSUserSpaceContext = { // TODO: Add this method to our main Emscripten FS types realPath(node: FSNode): string; }; - // TODO: Likely rename this. There's no reason it should be different from the rest of the names. - _js_wasm_trace: (...args: any[]) => void; }; export function bindUserSpace( @@ -115,13 +114,12 @@ export function bindUserSpace( LOCK_UN, }, errnoCodes: { EBADF, EINVAL, EAGAIN, EDEADLK, EWOULDBLOCK }, - wasmImports: { builtin_fcntl64, builtin_fd_close }, + wasmImports: { builtin_fcntl64, builtin_fd_close, js_wasm_trace }, wasmExports: { wasm_get_end_offset }, syscalls: { getStreamFromFD }, FS, PROXYFS, NODEFS, - _js_wasm_trace, }: OSUserSpaceContext ) { class VarArgsAccessor { @@ -164,7 +162,7 @@ export function bindUserSpace( [F_UNLCK as FcntlLockState]: 'unlocked', } as const satisfies Record, is_path_to_shared_fs(path: string) { - _js_wasm_trace('is_path_to_shared_fs(%s)', path); + js_wasm_trace('is_path_to_shared_fs(%s)', path); const { node } = FS.lookupPath(path, { noent_okay: true }); if (node.mount.type !== PROXYFS) { return !!node.isSharedFS; @@ -208,7 +206,7 @@ export function bindUserSpace( // TODO: Tolerate ENOENT here? const { node: backingNode, path: backingPath } = node.mount.opts.fs.lookupPath(vfsPath); - _js_wasm_trace( + js_wasm_trace( 'backingNode for %s: %s', vfsPath, backingPath, @@ -371,7 +369,7 @@ export function bindUserSpace( const stream = getStreamFromFD(fd); baseAddress = FS.llseek(stream, 0, whence); } catch (e) { - _js_wasm_trace( + js_wasm_trace( 'get_base_address(%d, %d, %d) getStreamFromFD error %s', fd, whence, @@ -407,11 +405,11 @@ export function bindUserSpace( async function fcntl64(fd: number, cmd: number, varargs?: number) { switch (cmd) { case F_GETLK: { - _js_wasm_trace('fcntl(%d, F_GETLK)', fd); + js_wasm_trace('fcntl(%d, F_GETLK)', fd); const [vfsPath, vfsPathErrno] = locking.get_vfs_path_from_fd(fd); if (vfsPathErrno !== 0) { - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_GETLK) %s get_vfs_path_from_fd errno %d', fd, vfsPath, @@ -424,7 +422,7 @@ export function bindUserSpace( const flockStructAddr = varArgsAccessor.getNextAsPointer(); if (!locking.is_path_to_shared_fs(vfsPath)) { - _js_wasm_trace( + js_wasm_trace( "fcntl(%d, F_GETLK) locking is not implemented for non-NodeFS path '%s'", fd, vfsPath @@ -449,7 +447,7 @@ export function bindUserSpace( flockStruct.l_type ); if (paramsCheckErrno !== 0) { - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_GETLK) %s check_lock_params errno %d', fd, vfsPath, @@ -467,7 +465,7 @@ export function bindUserSpace( flockStruct.l_start ); if (baseAddressErrno !== 0) { - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_GETLK) %s get_base_address errno %d', fd, vfsPath, @@ -490,7 +488,7 @@ export function bindUserSpace( } ); if (conflictingLock === undefined) { - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock type=unlocked start=0x%x end=0x%x', fd, vfsPath, @@ -504,7 +502,7 @@ export function bindUserSpace( return 0; } - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock type=%s start=0x%x end=0x%x conflictingLock %d', fd, vfsPath, @@ -527,7 +525,7 @@ export function bindUserSpace( }); return 0; } catch (e) { - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock error %s', fd, vfsPath, @@ -537,11 +535,11 @@ export function bindUserSpace( } } case F_SETLK: { - _js_wasm_trace('fcntl(%d, F_SETLK)', fd); + js_wasm_trace('fcntl(%d, F_SETLK)', fd); const [vfsPath, vfsPathErrno] = locking.get_vfs_path_from_fd(fd); if (vfsPathErrno !== 0) { - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_SETLK) %s get_vfs_path_from_fd errno %d', fd, vfsPath, @@ -551,7 +549,7 @@ export function bindUserSpace( } if (!locking.is_path_to_shared_fs(vfsPath)) { - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_SETLK) locking is not implemented for non-NodeFS path %s', fd, vfsPath @@ -573,7 +571,7 @@ export function bindUserSpace( flockStruct.l_start ); if (baseAddressErrno !== 0) { - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_SETLK) %s get_base_address errno %d', fd, vfsPath, @@ -583,7 +581,7 @@ export function bindUserSpace( } if (!(flockStruct.l_type in locking.fcntlToLockState)) { - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_SETLK) %s invalid lock type %d', fd, vfsPath, @@ -597,7 +595,7 @@ export function bindUserSpace( flockStruct.l_type ); if (paramsCheckErrno !== 0) { - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_SETLK) %s check_lock_params errno %d', fd, vfsPath, @@ -620,7 +618,7 @@ export function bindUserSpace( try { const nativeFilePath = locking.get_native_path_from_vfs_path(vfsPath); - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_SETLK) %s calling lockFileByteRange for range lock %s', fd, vfsPath, @@ -632,7 +630,7 @@ export function bindUserSpace( rangeLock ); - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_SETLK) %s lockFileByteRange returned %d for range lock %s', fd, vfsPath, @@ -641,7 +639,7 @@ export function bindUserSpace( ); return succeeded ? 0 : -EAGAIN; } catch (e) { - _js_wasm_trace( + js_wasm_trace( 'fcntl(%d, F_SETLK) %s lockFileByteRange error %s for range lock %s', fd, vfsPath, @@ -689,7 +687,7 @@ export function bindUserSpace( } async function flock(fd: number, op: number) { - _js_wasm_trace('js_flock(%d, %d)', fd, op); + js_wasm_trace('js_flock(%d, %d)', fd, op); type FlockOp = typeof LOCK_SH | typeof LOCK_EX | typeof LOCK_UN; const flockToLockOpType = { @@ -700,7 +698,7 @@ export function bindUserSpace( const [vfsPath, vfsPathErrno] = locking.get_vfs_path_from_fd(fd); if (vfsPathErrno !== 0) { - _js_wasm_trace( + js_wasm_trace( 'js_flock(%d, %d) get_vfs_path_from_fd errno %d', fd, op, @@ -711,7 +709,7 @@ export function bindUserSpace( } if (!locking.is_path_to_shared_fs(vfsPath)) { - _js_wasm_trace( + js_wasm_trace( 'flock(%d, %d) locking is not implemented for non-NodeFS path %s', fd, op, @@ -724,7 +722,7 @@ export function bindUserSpace( const paramsCheckErrno = locking.check_lock_params(fd, op); if (paramsCheckErrno !== 0) { - _js_wasm_trace( + js_wasm_trace( 'js_flock(%d, %d) check_lock_params errno %d', fd, op, @@ -735,7 +733,7 @@ export function bindUserSpace( // @TODO: Consider supporting blocking mode of flock() if ((op & LOCK_NB) === 0) { - _js_wasm_trace( + js_wasm_trace( 'js_flock(%d, %d) blocking mode of flock() is not implemented', fd, op @@ -749,21 +747,13 @@ export function bindUserSpace( const maskedOp = op & ((LOCK_SH | LOCK_EX | LOCK_UN) as FlockOp | 0); if (maskedOp === 0) { - _js_wasm_trace( - 'js_flock(%d, %d) invalid flock() operation', - fd, - op - ); + js_wasm_trace('js_flock(%d, %d) invalid flock() operation', fd, op); return -EINVAL; } const lockOpType = flockToLockOpType[maskedOp as FlockOp]; if (lockOpType === undefined) { - _js_wasm_trace( - 'js_flock(%d, %d) invalid flock() operation', - fd, - op - ); + js_wasm_trace('js_flock(%d, %d) invalid flock() operation', fd, op); return -EINVAL; } @@ -778,7 +768,7 @@ export function bindUserSpace( fd, } ); - _js_wasm_trace( + js_wasm_trace( 'js_flock(%d, %d) lockWholeFile %s returned %d', fd, op, @@ -787,12 +777,7 @@ export function bindUserSpace( ); return obtainedLock ? 0 : -EWOULDBLOCK; } catch (e) { - _js_wasm_trace( - 'js_flock(%d, %d) lockWholeFile error %s', - fd, - op, - e - ); + js_wasm_trace('js_flock(%d, %d) lockWholeFile error %s', fd, op, e); return -EINVAL; } } @@ -805,12 +790,12 @@ export function bindUserSpace( const fdCloseResult = builtin_fd_close(fd); if (fdCloseResult !== 0 || !locking.maybeLockedFds.has(fd)) { - _js_wasm_trace('fd_close(%d) result %d', fd, fdCloseResult); + js_wasm_trace('fd_close(%d) result %d', fd, fdCloseResult); return fdCloseResult; } if (vfsPathResolutionErrno !== 0) { - _js_wasm_trace( + js_wasm_trace( 'fd_close(%d) get_vfs_path_from_fd error %d', fd, vfsPathResolutionErrno @@ -836,9 +821,9 @@ export function bindUserSpace( fd, nativeFilePath ); - _js_wasm_trace('fd_close(%d) release locks success', fd); + js_wasm_trace('fd_close(%d) release locks success', fd); } catch (e) { - _js_wasm_trace("fd_close(%d) error '%s'", fd, e); + js_wasm_trace("fd_close(%d) error '%s'", fd, e); } finally { locking.maybeLockedFds.delete(fd); } @@ -848,17 +833,17 @@ export function bindUserSpace( // TODO: Implement based on current process // TODO: Replace with process exit handler async function js_release_file_locks() { - _js_wasm_trace('js_release_file_locks()'); + js_wasm_trace('js_release_file_locks()'); if (!pid || !fileLockManager) { - _js_wasm_trace('js_release_file_locks no pid or file lock manager'); + js_wasm_trace('js_release_file_locks no pid or file lock manager'); return; } try { await fileLockManager.releaseLocksForProcess(pid); - _js_wasm_trace('js_release_file_locks succeeded'); + js_wasm_trace('js_release_file_locks succeeded'); } catch (e) { - _js_wasm_trace('js_release_file_locks error %s', e); + js_wasm_trace('js_release_file_locks error %s', e); } } From bec0521fd4f00a377a87d4e31b86f761b310e7e3 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Thu, 13 Nov 2025 22:25:39 -0500 Subject: [PATCH 5/5] Explain why taking wasmImports, wasmExports, and syscalls collections --- packages/php-wasm/universal/src/lib/os-user-space.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/php-wasm/universal/src/lib/os-user-space.ts b/packages/php-wasm/universal/src/lib/os-user-space.ts index 256f9719b7..0bedaef592 100644 --- a/packages/php-wasm/universal/src/lib/os-user-space.ts +++ b/packages/php-wasm/universal/src/lib/os-user-space.ts @@ -64,14 +64,23 @@ export type OSUserSpaceContext = { HEAPU64: BigUint64Array; HEAPF64: Float64Array; }; + // This is a collection of functions present in built php-wasm JS. + // By receiving the entire collection here, we can avoid recompiling + // php-wasm JS whenever we add a new dependency from this collection. wasmImports: { builtin_fcntl64: (fd: number, cmd: number, varargs?: any) => number; builtin_fd_close: (fd: number) => number; js_wasm_trace: (...args: any[]) => void; }; + // This is a collection of functions present in built php-wasm JS. + // By receiving the entire collection here, we can avoid recompiling + // php-wasm JS whenever we add a new dependency from this collection. wasmExports: { wasm_get_end_offset: (fd: number) => bigint; }; + // This is a collection of functions present in built php-wasm JS. + // By receiving the entire collection here, we can avoid recompiling + // php-wasm JS whenever we add a new dependency from this collection. syscalls: { getStreamFromFD: (fd: number) => Emscripten.FS.FSStream; };