Proposal: std.fs Windows FFI Implementation

Status: Draft Created: 2026-01-30 Affects: Standard library Depends on: std.fs API proposal, Platform FFI proposal, Fixed-capacity lists proposal


Summary

This proposal adds Windows FFI implementation details to the approved std.fs API. File system operations on Windows are backed by Win32 APIs from kernel32.dll, with UTF-16 string handling and handle-based I/O.


Motivation

Windows file system APIs differ significantly from POSIX:

AspectPOSIXWindows
String encodingUTF-8UTF-16 (WCHAR)
File handlesint (fd)HANDLE (void*)
Error handlingerrnoGetLastError()
Path separators/\ (also accepts /)
Path length~4096260 (MAX_PATH) or extended
PermissionsMode bitsACLs

A dedicated Windows implementation ensures:

  1. Correct UTF-16 string marshalling
  2. Proper handle management
  3. Windows-specific error mapping
  4. Extended path support (>260 chars)

FFI Implementation

External Declarations

// std/fs/ffi_windows.ori (internal)
#target(os: "windows")

// Windows type aliases
type HANDLE = CPtr
type DWORD = int      // 32-bit unsigned, but int for simplicity
type BOOL = int       // Windows BOOL is int, not bool
type WCHAR = int      // 16-bit, stored as int for manipulation

// Invalid handle sentinel
let $INVALID_HANDLE_VALUE: HANDLE = CPtr.from_int(i: -1)

#repr("c")
type FILETIME = {
    dwLowDateTime: int,
    dwHighDateTime: int
}

#repr("c")
type WIN32_FIND_DATAW = {
    dwFileAttributes: DWORD,
    ftCreationTime: FILETIME,
    ftLastAccessTime: FILETIME,
    ftLastWriteTime: FILETIME,
    nFileSizeHigh: DWORD,
    nFileSizeLow: DWORD,
    dwReserved0: DWORD,
    dwReserved1: DWORD,
    cFileName: [WCHAR, max 260],
    cAlternateFileName: [WCHAR, max 14]
}

#repr("c")
type BY_HANDLE_FILE_INFORMATION = {
    dwFileAttributes: DWORD,
    ftCreationTime: FILETIME,
    ftLastAccessTime: FILETIME,
    ftLastWriteTime: FILETIME,
    dwVolumeSerialNumber: DWORD,
    nFileSizeHigh: DWORD,
    nFileSizeLow: DWORD,
    nNumberOfLinks: DWORD,
    nFileIndexHigh: DWORD,
    nFileIndexLow: DWORD
}

#repr("c")
type SECURITY_ATTRIBUTES = {
    nLength: DWORD,
    lpSecurityDescriptor: CPtr,
    bInheritHandle: BOOL
}

extern "c" from "kernel32" {
    // File operations
    @_CreateFileW (
        lpFileName: CPtr,           // LPCWSTR - pointer to UTF-16 string
        dwDesiredAccess: DWORD,
        dwShareMode: DWORD,
        lpSecurityAttributes: CPtr, // nullable
        dwCreationDisposition: DWORD,
        dwFlagsAndAttributes: DWORD,
        hTemplateFile: HANDLE       // nullable
    ) -> HANDLE as "CreateFileW"

    @_ReadFile (
        hFile: HANDLE,
        lpBuffer: [byte],
        nNumberOfBytesToRead: DWORD,
        lpNumberOfBytesRead: CPtr,  // out: DWORD*
        lpOverlapped: CPtr          // nullable
    ) -> BOOL as "ReadFile"

    @_WriteFile (
        hFile: HANDLE,
        lpBuffer: [byte],
        nNumberOfBytesToWrite: DWORD,
        lpNumberOfBytesWritten: CPtr,  // out: DWORD*
        lpOverlapped: CPtr              // nullable
    ) -> BOOL as "WriteFile"

    @_CloseHandle (hObject: HANDLE) -> BOOL as "CloseHandle"

    @_FlushFileBuffers (hFile: HANDLE) -> BOOL as "FlushFileBuffers"

    @_SetFilePointerEx (
        hFile: HANDLE,
        liDistanceToMove: int,      // LARGE_INTEGER (64-bit)
        lpNewFilePointer: CPtr,     // out: nullable
        dwMoveMethod: DWORD
    ) -> BOOL as "SetFilePointerEx"

    @_SetEndOfFile (hFile: HANDLE) -> BOOL as "SetEndOfFile"

    @_GetFileSizeEx (
        hFile: HANDLE,
        lpFileSize: CPtr            // out: LARGE_INTEGER*
    ) -> BOOL as "GetFileSizeEx"

    // File info
    @_GetFileAttributesW (lpFileName: CPtr) -> DWORD as "GetFileAttributesW"

    @_GetFileAttributesExW (
        lpFileName: CPtr,
        fInfoLevelId: int,          // GET_FILEEX_INFO_LEVELS
        lpFileInformation: CPtr     // out: WIN32_FILE_ATTRIBUTE_DATA*
    ) -> BOOL as "GetFileAttributesExW"

    @_GetFileInformationByHandle (
        hFile: HANDLE,
        lpFileInformation: BY_HANDLE_FILE_INFORMATION
    ) -> BOOL as "GetFileInformationByHandle"

    @_SetFileAttributesW (
        lpFileName: CPtr,
        dwFileAttributes: DWORD
    ) -> BOOL as "SetFileAttributesW"

    // Directory operations
    @_CreateDirectoryW (
        lpPathName: CPtr,
        lpSecurityAttributes: CPtr  // nullable
    ) -> BOOL as "CreateDirectoryW"

    @_RemoveDirectoryW (lpPathName: CPtr) -> BOOL as "RemoveDirectoryW"

    @_FindFirstFileW (
        lpFileName: CPtr,
        lpFindFileData: WIN32_FIND_DATAW
    ) -> HANDLE as "FindFirstFileW"

    @_FindNextFileW (
        hFindFile: HANDLE,
        lpFindFileData: WIN32_FIND_DATAW
    ) -> BOOL as "FindNextFileW"

    @_FindClose (hFindFile: HANDLE) -> BOOL as "FindClose"

    // File manipulation
    @_DeleteFileW (lpFileName: CPtr) -> BOOL as "DeleteFileW"

    @_MoveFileExW (
        lpExistingFileName: CPtr,
        lpNewFileName: CPtr,
        dwFlags: DWORD
    ) -> BOOL as "MoveFileExW"

    @_CopyFileExW (
        lpExistingFileName: CPtr,
        lpNewFileName: CPtr,
        lpProgressRoutine: CPtr,    // nullable
        lpData: CPtr,               // nullable
        pbCancel: CPtr,             // nullable
        dwCopyFlags: DWORD
    ) -> BOOL as "CopyFileExW"

    @_CreateHardLinkW (
        lpFileName: CPtr,
        lpExistingFileName: CPtr,
        lpSecurityAttributes: CPtr  // nullable
    ) -> BOOL as "CreateHardLinkW"

    @_CreateSymbolicLinkW (
        lpSymlinkFileName: CPtr,
        lpTargetFileName: CPtr,
        dwFlags: DWORD
    ) -> BOOL as "CreateSymbolicLinkW"

    // Path operations
    @_GetCurrentDirectoryW (
        nBufferLength: DWORD,
        lpBuffer: CPtr              // out: WCHAR*
    ) -> DWORD as "GetCurrentDirectoryW"

    @_SetCurrentDirectoryW (lpPathName: CPtr) -> BOOL as "SetCurrentDirectoryW"

    @_GetFullPathNameW (
        lpFileName: CPtr,
        nBufferLength: DWORD,
        lpBuffer: CPtr,             // out: WCHAR*
        lpFilePart: CPtr            // out: nullable WCHAR**
    ) -> DWORD as "GetFullPathNameW"

    @_GetTempPathW (
        nBufferLength: DWORD,
        lpBuffer: CPtr              // out: WCHAR*
    ) -> DWORD as "GetTempPathW"

    @_GetLongPathNameW (
        lpszShortPath: CPtr,
        lpszLongPath: CPtr,         // out: WCHAR*
        cchBuffer: DWORD
    ) -> DWORD as "GetLongPathNameW"

    // Temp files
    @_GetTempFileNameW (
        lpPathName: CPtr,
        lpPrefixString: CPtr,
        uUnique: int,
        lpTempFileName: CPtr        // out: WCHAR[MAX_PATH]
    ) -> int as "GetTempFileNameW"

    // Error handling
    @_GetLastError () -> DWORD as "GetLastError"

    @_FormatMessageW (
        dwFlags: DWORD,
        lpSource: CPtr,             // nullable
        dwMessageId: DWORD,
        dwLanguageId: DWORD,
        lpBuffer: CPtr,             // out: WCHAR*
        nSize: DWORD,
        Arguments: CPtr             // nullable
    ) -> DWORD as "FormatMessageW"

    @_LocalFree (hMem: CPtr) -> CPtr as "LocalFree"
}

// Access rights
let $GENERIC_READ: DWORD = 0x80000000
let $GENERIC_WRITE: DWORD = 0x40000000
let $GENERIC_EXECUTE: DWORD = 0x20000000
let $GENERIC_ALL: DWORD = 0x10000000

// Share modes
let $FILE_SHARE_READ: DWORD = 0x00000001
let $FILE_SHARE_WRITE: DWORD = 0x00000002
let $FILE_SHARE_DELETE: DWORD = 0x00000004

// Creation dispositions
let $CREATE_NEW: DWORD = 1
let $CREATE_ALWAYS: DWORD = 2
let $OPEN_EXISTING: DWORD = 3
let $OPEN_ALWAYS: DWORD = 4
let $TRUNCATE_EXISTING: DWORD = 5

// File attributes
let $FILE_ATTRIBUTE_READONLY: DWORD = 0x00000001
let $FILE_ATTRIBUTE_HIDDEN: DWORD = 0x00000002
let $FILE_ATTRIBUTE_SYSTEM: DWORD = 0x00000004
let $FILE_ATTRIBUTE_DIRECTORY: DWORD = 0x00000010
let $FILE_ATTRIBUTE_ARCHIVE: DWORD = 0x00000020
let $FILE_ATTRIBUTE_NORMAL: DWORD = 0x00000080
let $FILE_ATTRIBUTE_TEMPORARY: DWORD = 0x00000100
let $FILE_ATTRIBUTE_REPARSE_POINT: DWORD = 0x00000400

// Invalid file attributes (error indicator)
let $INVALID_FILE_ATTRIBUTES: DWORD = 0xFFFFFFFF

// Move flags
let $MOVEFILE_REPLACE_EXISTING: DWORD = 0x00000001
let $MOVEFILE_COPY_ALLOWED: DWORD = 0x00000002

// Copy flags
let $COPY_FILE_FAIL_IF_EXISTS: DWORD = 0x00000001

// Symbolic link flags
let $SYMBOLIC_LINK_FLAG_FILE: DWORD = 0x0
let $SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD = 0x1

// Seek methods
let $FILE_BEGIN: DWORD = 0
let $FILE_CURRENT: DWORD = 1
let $FILE_END: DWORD = 2

// FormatMessage flags
let $FORMAT_MESSAGE_ALLOCATE_BUFFER: DWORD = 0x00000100
let $FORMAT_MESSAGE_FROM_SYSTEM: DWORD = 0x00001000
let $FORMAT_MESSAGE_IGNORE_INSERTS: DWORD = 0x00000200

UTF-16 String Handling

Windows APIs use UTF-16 (wide strings). Ori strings are UTF-8.

Conversion Functions

// std/fs/ffi_windows_string.ori (internal)
#target(os: "windows")

// Convert Ori UTF-8 string to Windows UTF-16
// Returns pointer to null-terminated WCHAR array (allocated)
@utf8_to_utf16 (s: str) -> CPtr uses FFI =
    run(
        // Calculate required buffer size
        let utf8_bytes = s.as_bytes(),
        let max_wchars = len(collection: utf8_bytes) + 1,  // Worst case + null
        let buffer: [WCHAR, max 32768] = [],  // 32KB should handle most paths

        // Convert UTF-8 to UTF-16
        let wchar_count = utf8_to_utf16_impl(
            utf8: utf8_bytes,
            utf16: buffer,
            max_len: max_wchars
        ),

        // Return pointer to buffer
        buffer.as_ptr()
    )

// Low-level conversion (implemented in Ori or via FFI)
@utf8_to_utf16_impl (utf8: [byte], utf16: [WCHAR, max 32768], max_len: int) -> int =
    run(
        let i = 0,
        let j = 0,
        loop(
            if i >= len(collection: utf8) then break,

            let byte0 = utf8[i],

            // Decode UTF-8 codepoint
            let (codepoint, bytes_consumed) =
                if (byte0 & 0x80) == 0 then
                    // ASCII
                    (byte0 as int, 1)
                else if (byte0 & 0xE0) == 0xC0 then
                    // 2-byte sequence
                    let byte1 = utf8[i + 1],
                    (((byte0 & 0x1F) as int) << 6 | ((byte1 & 0x3F) as int), 2)
                else if (byte0 & 0xF0) == 0xE0 then
                    // 3-byte sequence
                    let byte1 = utf8[i + 1],
                    let byte2 = utf8[i + 2],
                    (((byte0 & 0x0F) as int) << 12 | ((byte1 & 0x3F) as int) << 6 | ((byte2 & 0x3F) as int), 3)
                else
                    // 4-byte sequence (surrogate pair needed)
                    let byte1 = utf8[i + 1],
                    let byte2 = utf8[i + 2],
                    let byte3 = utf8[i + 3],
                    (((byte0 & 0x07) as int) << 18 | ((byte1 & 0x3F) as int) << 12 | ((byte2 & 0x3F) as int) << 6 | ((byte3 & 0x3F) as int), 4),

            // Encode to UTF-16
            if codepoint <= 0xFFFF then
                run(
                    utf16[j] = codepoint,
                    j = j + 1
                )
            else
                // Surrogate pair for codepoints > 0xFFFF
                run(
                    let adjusted = codepoint - 0x10000,
                    let high_surrogate = 0xD800 + (adjusted >> 10),
                    let low_surrogate = 0xDC00 + (adjusted & 0x3FF),
                    utf16[j] = high_surrogate,
                    utf16[j + 1] = low_surrogate,
                    j = j + 2
                ),

            i = i + bytes_consumed,
            continue
        ),

        // Null terminate
        utf16[j] = 0,
        j
    )

// Convert Windows UTF-16 to Ori UTF-8 string
@utf16_to_utf8 (wstr: CPtr, len: int) -> str uses FFI =
    run(
        let buffer: [byte, max 65536] = [],
        let byte_count = utf16_to_utf8_impl(
            utf16: wstr,
            utf16_len: len,
            utf8: buffer,
            max_len: 65536
        ),
        str.from_utf8(bytes: buffer[0..byte_count]).unwrap_or(default: "")
    )

// Read null-terminated UTF-16 string from pointer
@utf16_to_utf8_null_terminated (wstr: CPtr) -> str uses FFI =
    run(
        // Find null terminator
        let len = 0,
        loop(
            let wchar = unsafe { ptr_read_u16(ptr: wstr, offset: len) },
            if wchar == 0 then break,
            len = len + 1,
            continue
        ),
        utf16_to_utf8(wstr: wstr, len: len)
    )

Extended Path Support

Windows has a 260 character path limit (MAX_PATH). For longer paths, use the \\?\ prefix:

// std/fs/ffi_windows_path.ori (internal)
#target(os: "windows")

// Convert path to extended-length format for paths > 260 chars
@to_extended_path (path: str) -> str =
    if len(collection: path) >= 260 && !path.starts_with(prefix: "\\\\?\\") then
        if path.starts_with(prefix: "\\\\") then
            // UNC path: \\server\share -> \\?\UNC\server\share
            "\\\\?\\UNC\\" + path[2..]
        else
            // Regular path: C:\foo -> \\?\C:\foo
            "\\\\?\\" + path
    else
        path

// Normalize path separators (/ -> \)
@normalize_path_separators (path: str) -> str =
    path.replace(from: "/", to: "\\")

// Prepare path for Windows API call
@prepare_win_path (path: str) -> CPtr uses FFI =
    run(
        let normalized = normalize_path_separators(path: path),
        let extended = to_extended_path(path: normalized),
        utf8_to_utf16(s: extended)
    )

Implementation Mapping

Reading Files

// std/fs/read_windows.ori
#target(os: "windows")
use "./ffi_windows" { ... }
use "./ffi_windows_string" { prepare_win_path, utf16_to_utf8 }
use "./error_windows" { last_error_to_file_error }

pub @read (path: str) -> Result<str, FileError> uses FileSystem =
    run(
        let wpath = prepare_win_path(path: path),
        let handle = _CreateFileW(
            lpFileName: wpath,
            dwDesiredAccess: $GENERIC_READ,
            dwShareMode: $FILE_SHARE_READ,
            lpSecurityAttributes: CPtr.null(),
            dwCreationDisposition: $OPEN_EXISTING,
            dwFlagsAndAttributes: $FILE_ATTRIBUTE_NORMAL,
            hTemplateFile: CPtr.null()
        ),

        if handle == $INVALID_HANDLE_VALUE then
            Err(last_error_to_file_error(path: Path.from_str(s: path)))
        else
            run(
                // Get file size
                let size_out: [int, max 1] = [0],
                let size_ok = _GetFileSizeEx(hFile: handle, lpFileSize: size_out.as_ptr()),
                if size_ok == 0 then
                    run(
                        _CloseHandle(hObject: handle),
                        Err(last_error_to_file_error(path: Path.from_str(s: path)))
                    )
                else
                    run(
                        let size = size_out[0],
                        let buf: [byte, max 1048576] = [],  // 1MB max inline read
                        let bytes_read_out: [int, max 1] = [0],

                        let read_ok = _ReadFile(
                            hFile: handle,
                            lpBuffer: buf,
                            nNumberOfBytesToRead: min(left: size, right: 1048576),
                            lpNumberOfBytesRead: bytes_read_out.as_ptr(),
                            lpOverlapped: CPtr.null()
                        ),

                        _CloseHandle(hObject: handle),

                        if read_ok == 0 then
                            Err(last_error_to_file_error(path: Path.from_str(s: path)))
                        else
                            str.from_utf8(bytes: buf[0..bytes_read_out[0]])
                                .map_err(transform: e -> FileError {
                                    kind: IoError,
                                    path: Path.from_str(s: path),
                                    message: "Invalid UTF-8"
                                })
                    )
            )
    )

pub @read_bytes (path: str) -> Result<[byte], FileError> uses FileSystem =
    run(
        let wpath = prepare_win_path(path: path),
        let handle = _CreateFileW(
            lpFileName: wpath,
            dwDesiredAccess: $GENERIC_READ,
            dwShareMode: $FILE_SHARE_READ,
            lpSecurityAttributes: CPtr.null(),
            dwCreationDisposition: $OPEN_EXISTING,
            dwFlagsAndAttributes: $FILE_ATTRIBUTE_NORMAL,
            hTemplateFile: CPtr.null()
        ),

        if handle == $INVALID_HANDLE_VALUE then
            Err(last_error_to_file_error(path: Path.from_str(s: path)))
        else
            run(
                let size_out: [int, max 1] = [0],
                _GetFileSizeEx(hFile: handle, lpFileSize: size_out.as_ptr()),
                let size = size_out[0],
                let buf: [byte, max 1048576] = [],
                let bytes_read_out: [int, max 1] = [0],

                let read_ok = _ReadFile(
                    hFile: handle,
                    lpBuffer: buf,
                    nNumberOfBytesToRead: min(left: size, right: 1048576),
                    lpNumberOfBytesRead: bytes_read_out.as_ptr(),
                    lpOverlapped: CPtr.null()
                ),

                _CloseHandle(hObject: handle),

                if read_ok == 0 then
                    Err(last_error_to_file_error(path: Path.from_str(s: path)))
                else
                    Ok(buf[0..bytes_read_out[0]].to_dynamic())
            )
    )

Writing Files

// std/fs/write_windows.ori
#target(os: "windows")
use "./ffi_windows" { ... }
use "./ffi_windows_string" { prepare_win_path }
use "./error_windows" { last_error_to_file_error }

pub @write (path: str, content: str) -> Result<void, FileError> uses FileSystem =
    write_bytes(path: path, content: content.as_bytes())

pub @write_bytes (path: str, content: [byte]) -> Result<void, FileError> uses FileSystem =
    run(
        let wpath = prepare_win_path(path: path),
        let handle = _CreateFileW(
            lpFileName: wpath,
            dwDesiredAccess: $GENERIC_WRITE,
            dwShareMode: 0,  // Exclusive access for writing
            lpSecurityAttributes: CPtr.null(),
            dwCreationDisposition: $CREATE_ALWAYS,
            dwFlagsAndAttributes: $FILE_ATTRIBUTE_NORMAL,
            hTemplateFile: CPtr.null()
        ),

        if handle == $INVALID_HANDLE_VALUE then
            Err(last_error_to_file_error(path: Path.from_str(s: path)))
        else
            run(
                let bytes_written_out: [int, max 1] = [0],
                let write_ok = _WriteFile(
                    hFile: handle,
                    lpBuffer: content,
                    nNumberOfBytesToWrite: len(collection: content),
                    lpNumberOfBytesWritten: bytes_written_out.as_ptr(),
                    lpOverlapped: CPtr.null()
                ),

                _CloseHandle(hObject: handle),

                if write_ok == 0 then
                    Err(last_error_to_file_error(path: Path.from_str(s: path)))
                else
                    Ok(())
            )
    )

pub @write_with (
    path: str,
    content: str,
    mode: WriteMode = Truncate,
    create_dirs: bool = false
) -> Result<void, FileError> uses FileSystem =
    run(
        if create_dirs then
            run(
                let parent = Path.from_str(s: path).parent(),
                match(parent,
                    Some(p) -> create_dir_all(path: p.to_str())?,
                    None -> ()
                )
            ),

        let disposition = match(mode,
            Create -> $CREATE_NEW,
            Append -> $OPEN_ALWAYS,
            Truncate -> $CREATE_ALWAYS
        ),

        let wpath = prepare_win_path(path: path),
        let handle = _CreateFileW(
            lpFileName: wpath,
            dwDesiredAccess: $GENERIC_WRITE,
            dwShareMode: 0,
            lpSecurityAttributes: CPtr.null(),
            dwCreationDisposition: disposition,
            dwFlagsAndAttributes: $FILE_ATTRIBUTE_NORMAL,
            hTemplateFile: CPtr.null()
        ),

        if handle == $INVALID_HANDLE_VALUE then
            Err(last_error_to_file_error(path: Path.from_str(s: path)))
        else
            run(
                // For append mode, seek to end
                if mode == Append then
                    _SetFilePointerEx(
                        hFile: handle,
                        liDistanceToMove: 0,
                        lpNewFilePointer: CPtr.null(),
                        dwMoveMethod: $FILE_END
                    ),

                let bytes = content.as_bytes(),
                let bytes_written_out: [int, max 1] = [0],
                let write_ok = _WriteFile(
                    hFile: handle,
                    lpBuffer: bytes,
                    nNumberOfBytesToWrite: len(collection: bytes),
                    lpNumberOfBytesWritten: bytes_written_out.as_ptr(),
                    lpOverlapped: CPtr.null()
                ),

                _CloseHandle(hObject: handle),

                if write_ok == 0 then
                    Err(last_error_to_file_error(path: Path.from_str(s: path)))
                else
                    Ok(())
            )
    )

Directory Operations

// std/fs/dir_windows.ori
#target(os: "windows")
use "./ffi_windows" { ... }
use "./ffi_windows_string" { prepare_win_path, utf16_to_utf8_null_terminated }
use "./error_windows" { last_error_to_file_error }

pub @list_dir (path: str) -> Result<[str], FileError> uses FileSystem =
    run(
        // Append \* for FindFirstFile pattern
        let pattern = path + "\\*",
        let wpattern = prepare_win_path(path: pattern),

        let find_data = WIN32_FIND_DATAW.zeroed(),
        let handle = _FindFirstFileW(lpFileName: wpattern, lpFindFileData: find_data),

        if handle == $INVALID_HANDLE_VALUE then
            Err(last_error_to_file_error(path: Path.from_str(s: path)))
        else
            run(
                let entries: [str, max 4096] = [],

                // Process first result
                let name = utf16_to_utf8_null_terminated(wstr: find_data.cFileName.as_ptr()),
                if name != "." && name != ".." then
                    entries.push(name),

                // Process remaining results
                loop(
                    let find_data_next = WIN32_FIND_DATAW.zeroed(),
                    let found = _FindNextFileW(hFindFile: handle, lpFindFileData: find_data_next),
                    if found == 0 then break,

                    let name_next = utf16_to_utf8_null_terminated(wstr: find_data_next.cFileName.as_ptr()),
                    if name_next != "." && name_next != ".." then
                        entries.push(name_next),
                    continue
                ),

                _FindClose(hFindFile: handle),
                Ok(entries.to_dynamic())
            )
    )

pub @create_dir (path: str) -> Result<void, FileError> uses FileSystem =
    run(
        let wpath = prepare_win_path(path: path),
        let result = _CreateDirectoryW(lpPathName: wpath, lpSecurityAttributes: CPtr.null()),
        if result == 0 then
            Err(last_error_to_file_error(path: Path.from_str(s: path)))
        else
            Ok(())
    )

pub @create_dir_all (path: str) -> Result<void, FileError> uses FileSystem =
    run(
        let parts = Path.from_str(s: path).segments,
        let current = if Path.from_str(s: path).is_absolute() then
            // Windows absolute path starts with drive letter
            parts[0]
        else
            "",

        let start_idx = if Path.from_str(s: path).is_absolute() then 1 else 0,

        for i in start_idx..len(collection: parts) do
            run(
                current = current + "\\" + parts[i],
                if !exists(path: current) then
                    create_dir(path: current)?
            ),
        Ok(())
    )

pub @remove_dir (path: str) -> Result<void, FileError> uses FileSystem =
    run(
        let wpath = prepare_win_path(path: path),
        let result = _RemoveDirectoryW(lpPathName: wpath),
        if result == 0 then
            Err(last_error_to_file_error(path: Path.from_str(s: path)))
        else
            Ok(())
    )

File Info

// std/fs/info_windows.ori
#target(os: "windows")
use "./ffi_windows" { ... }
use "./ffi_windows_string" { prepare_win_path }
use "./error_windows" { last_error_to_file_error }
use std.time { Instant }

pub @info (path: str) -> Result<FileInfo, FileError> uses FileSystem =
    run(
        let wpath = prepare_win_path(path: path),

        // Use GetFileAttributesExW for basic info
        let attrs = _GetFileAttributesW(lpFileName: wpath),
        if attrs == $INVALID_FILE_ATTRIBUTES then
            Err(last_error_to_file_error(path: Path.from_str(s: path)))
        else
            // Open file to get detailed info
            run(
                let handle = _CreateFileW(
                    lpFileName: wpath,
                    dwDesiredAccess: 0,  // No access needed for metadata
                    dwShareMode: $FILE_SHARE_READ | $FILE_SHARE_WRITE | $FILE_SHARE_DELETE,
                    lpSecurityAttributes: CPtr.null(),
                    dwCreationDisposition: $OPEN_EXISTING,
                    dwFlagsAndAttributes: $FILE_ATTRIBUTE_NORMAL,
                    hTemplateFile: CPtr.null()
                ),

                if handle == $INVALID_HANDLE_VALUE then
                    Err(last_error_to_file_error(path: Path.from_str(s: path)))
                else
                    run(
                        let file_info = BY_HANDLE_FILE_INFORMATION.zeroed(),
                        let info_ok = _GetFileInformationByHandle(
                            hFile: handle,
                            lpFileInformation: file_info
                        ),

                        _CloseHandle(hObject: handle),

                        if info_ok == 0 then
                            Err(last_error_to_file_error(path: Path.from_str(s: path)))
                        else
                            Ok(win32_to_file_info(path: path, attrs: attrs, info: file_info))
                    )
            )
    )

@win32_to_file_info (path: str, attrs: DWORD, info: BY_HANDLE_FILE_INFORMATION) -> FileInfo =
    FileInfo {
        path: Path.from_str(s: path),
        size: (info.nFileSizeHigh as int) << 32 | (info.nFileSizeLow as int),
        is_file: (attrs & $FILE_ATTRIBUTE_DIRECTORY) == 0,
        is_dir: (attrs & $FILE_ATTRIBUTE_DIRECTORY) != 0,
        is_symlink: (attrs & $FILE_ATTRIBUTE_REPARSE_POINT) != 0,
        modified: filetime_to_instant(ft: info.ftLastWriteTime),
        created: Some(filetime_to_instant(ft: info.ftCreationTime)),
        readonly: (attrs & $FILE_ATTRIBUTE_READONLY) != 0
    }

// Convert Windows FILETIME (100-nanosecond intervals since Jan 1, 1601) to Instant
@filetime_to_instant (ft: FILETIME) -> Instant =
    run(
        // FILETIME is 64-bit value split into two 32-bit parts
        let ticks = (ft.dwHighDateTime as int) << 32 | (ft.dwLowDateTime as int),
        // Difference between Windows epoch (1601) and Unix epoch (1970) in 100-ns intervals
        let windows_to_unix_ticks: int = 116444736000000000,
        let unix_ticks = ticks - windows_to_unix_ticks,
        // Convert 100-ns intervals to seconds
        let secs = unix_ticks / 10000000,
        Instant.from_unix_secs(secs: secs)
    )

pub @exists (path: str) -> bool uses FileSystem =
    run(
        let wpath = prepare_win_path(path: path),
        let attrs = _GetFileAttributesW(lpFileName: wpath),
        attrs != $INVALID_FILE_ATTRIBUTES
    )

pub @is_file (path: str) -> bool uses FileSystem =
    run(
        let wpath = prepare_win_path(path: path),
        let attrs = _GetFileAttributesW(lpFileName: wpath),
        attrs != $INVALID_FILE_ATTRIBUTES && (attrs & $FILE_ATTRIBUTE_DIRECTORY) == 0
    )

pub @is_dir (path: str) -> bool uses FileSystem =
    run(
        let wpath = prepare_win_path(path: path),
        let attrs = _GetFileAttributesW(lpFileName: wpath),
        attrs != $INVALID_FILE_ATTRIBUTES && (attrs & $FILE_ATTRIBUTE_DIRECTORY) != 0
    )

Error Mapping

// std/fs/error_windows.ori
#target(os: "windows")
use "./ffi_windows" { _GetLastError, _FormatMessageW, _LocalFree, $FORMAT_MESSAGE_ALLOCATE_BUFFER, $FORMAT_MESSAGE_FROM_SYSTEM, $FORMAT_MESSAGE_IGNORE_INSERTS }
use "./ffi_windows_string" { utf16_to_utf8_null_terminated }

// Windows error codes
let $ERROR_FILE_NOT_FOUND: DWORD = 2
let $ERROR_PATH_NOT_FOUND: DWORD = 3
let $ERROR_ACCESS_DENIED: DWORD = 5
let $ERROR_INVALID_HANDLE: DWORD = 6
let $ERROR_NOT_ENOUGH_MEMORY: DWORD = 8
let $ERROR_INVALID_ACCESS: DWORD = 12
let $ERROR_INVALID_DATA: DWORD = 13
let $ERROR_OUTOFMEMORY: DWORD = 14
let $ERROR_FILE_EXISTS: DWORD = 80
let $ERROR_CANNOT_MAKE: DWORD = 82
let $ERROR_INVALID_PARAMETER: DWORD = 87
let $ERROR_BROKEN_PIPE: DWORD = 109
let $ERROR_DISK_FULL: DWORD = 112
let $ERROR_INVALID_NAME: DWORD = 123
let $ERROR_DIR_NOT_EMPTY: DWORD = 145
let $ERROR_ALREADY_EXISTS: DWORD = 183
let $ERROR_FILENAME_EXCED_RANGE: DWORD = 206
let $ERROR_DIRECTORY: DWORD = 267

@last_error_to_file_error (path: Path) -> FileError uses FFI =
    run(
        let err = _GetLastError(),
        let kind = match(err,
            $ERROR_FILE_NOT_FOUND -> NotFound,
            $ERROR_PATH_NOT_FOUND -> NotFound,
            $ERROR_ACCESS_DENIED -> PermissionDenied,
            $ERROR_INVALID_ACCESS -> PermissionDenied,
            $ERROR_FILE_EXISTS -> AlreadyExists,
            $ERROR_ALREADY_EXISTS -> AlreadyExists,
            $ERROR_DIR_NOT_EMPTY -> DirectoryNotEmpty,
            $ERROR_DIRECTORY -> NotAFile,
            $ERROR_INVALID_NAME -> InvalidPath,
            $ERROR_FILENAME_EXCED_RANGE -> InvalidPath,
            _ -> IoError
        ),
        FileError {
            kind: kind,
            path: path,
            message: format_win_error(err: err)
        }
    )

@format_win_error (err: DWORD) -> str uses FFI =
    run(
        let buffer_ptr: [CPtr, max 1] = [CPtr.null()],
        let len = _FormatMessageW(
            dwFlags: $FORMAT_MESSAGE_ALLOCATE_BUFFER | $FORMAT_MESSAGE_FROM_SYSTEM | $FORMAT_MESSAGE_IGNORE_INSERTS,
            lpSource: CPtr.null(),
            dwMessageId: err,
            dwLanguageId: 0,  // Default language
            lpBuffer: buffer_ptr.as_ptr(),
            nSize: 0,
            Arguments: CPtr.null()
        ),

        if len == 0 || buffer_ptr[0].is_null() then
            `Windows error {err}`
        else
            run(
                let msg = utf16_to_utf8_null_terminated(wstr: buffer_ptr[0]),
                _LocalFree(hMem: buffer_ptr[0]),
                // Remove trailing newlines
                msg.trim_end()
            )
    )

FFI Struct Zeroed Methods

// std/fs/ffi_windows_zeroed.ori (internal)
#target(os: "windows")

impl FILETIME {
    @zeroed () -> FILETIME =
        FILETIME { dwLowDateTime: 0, dwHighDateTime: 0 }
}

impl WIN32_FIND_DATAW {
    @zeroed () -> WIN32_FIND_DATAW =
        WIN32_FIND_DATAW {
            dwFileAttributes: 0,
            ftCreationTime: FILETIME.zeroed(),
            ftLastAccessTime: FILETIME.zeroed(),
            ftLastWriteTime: FILETIME.zeroed(),
            nFileSizeHigh: 0,
            nFileSizeLow: 0,
            dwReserved0: 0,
            dwReserved1: 0,
            cFileName: [0; 260],
            cAlternateFileName: [0; 14]
        }
}

impl BY_HANDLE_FILE_INFORMATION {
    @zeroed () -> BY_HANDLE_FILE_INFORMATION =
        BY_HANDLE_FILE_INFORMATION {
            dwFileAttributes: 0,
            ftCreationTime: FILETIME.zeroed(),
            ftLastAccessTime: FILETIME.zeroed(),
            ftLastWriteTime: FILETIME.zeroed(),
            dwVolumeSerialNumber: 0,
            nFileSizeHigh: 0,
            nFileSizeLow: 0,
            nNumberOfLinks: 0,
            nFileIndexHigh: 0,
            nFileIndexLow: 0
        }
}

Build Configuration

# ori.toml
[native.windows]
libraries = ["kernel32"]

Platform Selection

The std.fs module selects implementation based on target platform:

// std/fs/mod.ori

// POSIX implementation
#target(family: "unix")
use "./read_posix" { read, read_bytes }
use "./write_posix" { write, write_bytes, write_with }
use "./dir_posix" { list_dir, create_dir, create_dir_all, remove_dir }
use "./info_posix" { info, exists, is_file, is_dir }

// Windows implementation
#target(os: "windows")
use "./read_windows" { read, read_bytes }
use "./write_windows" { write, write_bytes, write_with }
use "./dir_windows" { list_dir, create_dir, create_dir_all, remove_dir }
use "./info_windows" { info, exists, is_file, is_dir }

// Common types (pure Ori, no FFI)
pub use "./types" { Path, FileInfo, FileError, FileErrorKind, WriteMode, Permissions }
pub use "./path" { Path }

Differences from POSIX Implementation

AspectPOSIXWindows
String encodingUTF-8 nativeUTF-8 ↔ UTF-16 conversion
File handlesint (fd)HANDLE (CPtr)
Error handlingerrno + strerrorGetLastError + FormatMessage
Path separators/ only\ (also accepts /)
Extended pathsN/A\\?\ prefix for >260 chars
Creation timeOften unavailableAlways available
PermissionsMode bitsSimplified (readonly flag only)
Case sensitivityUsually sensitiveUsually insensitive

Future Considerations

ACL Support

Windows Access Control Lists provide fine-grained permissions beyond the simple readonly flag. A future proposal could add:

type WinPermissions = {
    owner: Principal,
    acl: [AclEntry]
}

@get_acl (path: str) -> Result<WinPermissions, FileError> uses FileSystem
@set_acl (path: str, permissions: WinPermissions) -> Result<void, FileError> uses FileSystem

Alternate Data Streams

NTFS supports alternate data streams (ADS):

// Read from alternate stream
read(path: "file.txt:stream_name")

Junction Points and Reparse Points

Windows has multiple symlink-like features that could be exposed:

type ReparseType = Symlink | Junction | MountPoint

@create_junction (target: str, link: str) -> Result<void, FileError> uses FileSystem

Summary

ComponentImplementation
File I/OCreateFileW, ReadFile, WriteFile, CloseHandle
Directory opsCreateDirectoryW, RemoveDirectoryW, FindFirstFileW/FindNextFileW
File infoGetFileAttributesW, GetFileInformationByHandle
Error handlingGetLastError, FormatMessageW
String encodingManual UTF-8 ↔ UTF-16 conversion
Extended pathsAutomatic \\?\ prefix for long paths
PermissionsSimplified to readonly flag (ACLs deferred)