diff --git a/Sources/System/FileOperations.swift b/Sources/System/FileOperations.swift index 1193a04a..bbb2b5fc 100644 --- a/Sources/System/FileOperations.swift +++ b/Sources/System/FileOperations.swift @@ -393,4 +393,181 @@ extension FileDescriptor { }.map { _ in (.init(rawValue: fds.0), .init(rawValue: fds.1)) } } #endif + +#if !os(Windows) + // MARK: Memory Mapping + + /// Describes the desired memory protection of the + /// mapping (and must not conflict with the open mode of the file). + /// Flags can be the bitwise OR of one or more of each case. + @frozen + public struct MemoryProtection: RawRepresentable, Hashable, Codable { + /// The raw C protection number. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a strongly typed error number from a raw C error number. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: raw) } + + /// Pages may not be accessed. + @_alwaysEmitIntoClient + public static var none: MemoryProtection { MemoryProtection(rawValue: _PROT_NONE) } + /// Pages may be read. + @_alwaysEmitIntoClient + public static var read: MemoryProtection { MemoryProtection(rawValue: _PROT_READ) } + /// Pages may be written. + @_alwaysEmitIntoClient + public static var write: MemoryProtection { MemoryProtection(rawValue: _PROT_WRITE) } + /// Pages may be executed. + @_alwaysEmitIntoClient + public static var executed: MemoryProtection { MemoryProtection(rawValue: _PROT_EXEC) } + } + + /// Determines whether updates to the mapping are + /// visible to other processes mapping the same region, and whether + /// updates are carried through to the underlying file. This + /// behavior is determined by exactly one flag. + public struct MemoryMapKind: RawRepresentable, Hashable, Codable { + /// The raw C flag number. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a strongly typed error number from a raw C error number. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: raw) } + + /// Share this mapping. Updates to the mapping are visible to + /// other processes mapping the same region, and (in the case + /// of file-backed mappings) are carried through to the + /// underlying file. + @_alwaysEmitIntoClient + public static var shared: MemoryMapKind { MemoryMapKind(rawValue: _MAP_SHARED) } + /// Create a private copy-on-write mapping. Updates to the + /// mapping are not visible to other processes mapping the + /// same file, and are not carried through to the underlying + /// file. It is unspecified whether changes made to the file + /// after the `memoryMap` call are visible in the mapped region. + @_alwaysEmitIntoClient + public static var `private`: MemoryMapKind { MemoryMapKind(rawValue: _MAP_PRIVATE) } + + // TODO: There are several other MemoryMapKinds. + } + + /// Determines whether memory sync should be + /// synchronous, asynchronous. + @frozen + public struct MemorySyncKind: RawRepresentable, Hashable, Codable { + /// The raw C flag number. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a strongly typed error number from a raw C error number. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: raw) } + + /// Requests an update and waits for it to complete. + @_alwaysEmitIntoClient + public static var synchronous: MemorySyncKind { MemorySyncKind(rawValue: _MS_SYNC) } + /// Specifies that an update be scheduled, but the call + /// returns immediately. + @_alwaysEmitIntoClient + public static var asynchronous: MemorySyncKind { MemorySyncKind(rawValue: _MS_ASYNC) } + } + + /// Create a new mapping in the virtual address space of the + /// calling process. + /// After the `memoryMap` call has returned, the file descriptor can + /// be closed immediately without invalidating the mapping. + /// - Parameters: + /// - length: Specifies the length of the mapping (which must be greater than 0). + /// - pageOffset: The page offset to map. Page size is determined by `sysconf(_SC_PAGE_SIZE)` + /// - kind: Determines the kind of mapping returned. Currently limited to `MAP_SHARED` and `MAP_PRIVATE`. + /// - protection: Describes the desired memory protection of the mapping (and must not conflict with the open mode of the file). + /// - Returns: The new memory mapping. + @_alwaysEmitIntoClient + public func memoryMap( + length: Int, pageOffset: Int, kind: MemoryMapKind, protection: [MemoryProtection] + ) throws -> UnsafeMutableRawPointer { + try _memoryMap(length: length, + pageOffset: pageOffset, + kind: kind, + protection: protection).get() + } + + @usableFromInline + internal func _memoryMap( + length: Int, pageOffset: Int, kind: MemoryMapKind, protection: [MemoryProtection] + ) throws -> Result { + valueOrErrno(valueOnFail: _MAP_FAILED, retryOnInterrupt: false) { + system_mmap(self.rawValue, length, protection.reduce(into: Int32(), { partialResult, prot in + partialResult |= prot.rawValue + }), kind.rawValue, _COffT(pageOffset)) + } + } + + /// Deletes the mappings for the specified + /// mapping, and causes further references to addresses within + /// the range to generate invalid memory references. The region is + /// also automatically unmapped when the process is terminated. On + /// the other hand, closing the file descriptor does not unmap the + /// region. + /// - Parameters: + /// - memoryMap: The memory map to unmap + /// - length: Amount in bytes to unmap. + @_alwaysEmitIntoClient + public func memoryUnmap(memoryMap: UnsafeMutableRawPointer, length: Int) throws { + _ = try _memoryUnmap(memoryMap: memoryMap, length: length).get() + } + + @usableFromInline + internal func _memoryUnmap(memoryMap: UnsafeMutableRawPointer, length: Int) throws -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_munmap(memoryMap, length) + } + } + + /// Flushes changes made to the in-core copy of a file that + /// was mapped into memory using `memoryMap` back to the filesystem. + /// Without use of this call, there is no guarantee that changes are + /// written back before `memoryUnmap` is called. To be more precise, the + /// part of the file that corresponds to the memory area at the start + /// of the map and having length of `length` is updated. + /// - Parameters: + /// - memoryMap: The memory map to sync. + /// - length: Length to update. + /// - kind: Should specify one of `MemorySyncKind.synchronous` or `MemorySyncKind.asynchronous`. + /// - invalidateOtherMappings: Asks to invalidate other mappings of the same file (so + /// that they can be updated with the fresh values just + /// written). + @_alwaysEmitIntoClient + public func memorySync( + memoryMap: UnsafeMutableRawPointer, + length: Int, + kind: MemorySyncKind, + invalidateOtherMappings: Bool = false + ) throws { + _ = try _memorySync(memoryMap: memoryMap, + length: length, + flags: invalidateOtherMappings ? kind.rawValue & _MS_INVALIDATE : kind.rawValue) + .get() + } + + @usableFromInline + internal func _memorySync(memoryMap: UnsafeMutableRawPointer, length: Int, flags: CInt) throws -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_msync(memoryMap, length, flags) + } + } +#endif } + diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index a4dbb89c..c9470b07 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -441,6 +441,26 @@ internal var _ELAST: CInt { ELAST } // MARK: File Operations +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _MAP_SHARED: CInt { MAP_SHARED } + +@_alwaysEmitIntoClient +internal var _MAP_PRIVATE: CInt { MAP_PRIVATE } + +@_alwaysEmitIntoClient +internal var _MAP_FAILED: UnsafeMutableRawPointer { MAP_FAILED } + +@_alwaysEmitIntoClient +internal var _MS_ASYNC: CInt { MS_ASYNC } + +@_alwaysEmitIntoClient +internal var _MS_SYNC: CInt { MS_SYNC } + +@_alwaysEmitIntoClient +internal var _MS_INVALIDATE: CInt { MS_INVALIDATE } +#endif + @_alwaysEmitIntoClient internal var _O_RDONLY: CInt { O_RDONLY } @@ -512,6 +532,21 @@ internal var _O_SYMLINK: CInt { O_SYMLINK } internal var _O_CLOEXEC: CInt { O_CLOEXEC } #endif +// MARK: Mmap Protection +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _PROT_EXEC: CInt { PROT_EXEC } + +@_alwaysEmitIntoClient +internal var _PROT_READ: CInt { PROT_READ } + +@_alwaysEmitIntoClient +internal var _PROT_WRITE: CInt { PROT_WRITE } + +@_alwaysEmitIntoClient +internal var _PROT_NONE: CInt { PROT_NONE } +#endif + @_alwaysEmitIntoClient internal var _SEEK_SET: CInt { SEEK_SET } diff --git a/Sources/System/Internals/Mocking.swift b/Sources/System/Internals/Mocking.swift index eb1fe834..92efa586 100644 --- a/Sources/System/Internals/Mocking.swift +++ b/Sources/System/Internals/Mocking.swift @@ -144,11 +144,13 @@ private func originalSyscallName(_ function: String) -> String { return String(function.dropFirst("system_".count).prefix { $0 != "(" }) } -private func mockImpl( +private func mockImpl( + valueOnFail: T, + valueOnSuccess: T, name: String, path: UnsafePointer?, _ args: [AnyHashable] -) -> CInt { +) -> T { precondition(mockingEnabled) let origName = originalSyscallName(name) guard let driver = currentMockingDriver else { @@ -165,32 +167,37 @@ private func mockImpl( case .none: break case .always(let e): system_errno = e - return -1 + return valueOnFail case .counted(let e, let count): assert(count >= 1) system_errno = e driver.forceErrno = count > 1 ? .counted(errno: e, count: count-1) : .none - return -1 + return valueOnFail } - return 0 + return valueOnSuccess } internal func _mock( name: String = #function, path: UnsafePointer? = nil, _ args: AnyHashable... ) -> CInt { - return mockImpl(name: name, path: path, args) + return mockImpl(valueOnFail: -1, valueOnSuccess: 0, name: name, path: path, args) } internal func _mockInt( name: String = #function, path: UnsafePointer? = nil, _ args: AnyHashable... ) -> Int { - Int(mockImpl(name: name, path: path, args)) + Int(mockImpl(valueOnFail: -1, valueOnSuccess: 0, name: name, path: path, args)) } internal func _mockOffT( name: String = #function, path: UnsafePointer? = nil, _ args: AnyHashable... ) -> _COffT { - _COffT(mockImpl(name: name, path: path, args)) + _COffT(mockImpl(valueOnFail: -1, valueOnSuccess: 0, name: name, path: path, args)) +} +internal func _mock( + valueOnFail: T, valueOnSuccess: T, name: String = #function, path: UnsafePointer? = nil, _ args: AnyHashable... +) -> T { + mockImpl(valueOnFail: valueOnFail, valueOnSuccess: valueOnSuccess, name: name, path: path, args) } #endif // ENABLE_MOCKING diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift index 453c02fc..27e11f3a 100644 --- a/Sources/System/Internals/Syscalls.swift +++ b/Sources/System/Internals/Syscalls.swift @@ -123,3 +123,36 @@ internal func system_pipe(_ fds: UnsafeMutablePointer) -> CInt { return pipe(fds) } #endif + +#if !os(Windows) +internal func system_mmap(_ fd: Int32, _ length: Int, _ prot: Int32, _ flags: Int32, _ offset: off_t) -> UnsafeMutableRawPointer { + #if ENABLE_MOCKING + if mockingEnabled { + let ptr = UnsafeMutableRawPointer.allocate(byteCount: 0, alignment: 0) + defer { ptr.deallocate() } + return _mock(valueOnFail: MAP_FAILED, + valueOnSuccess: ptr, + fd, length, prot, flags, offset) + } + #endif + return mmap(nil, length, prot, flags, fd, off_t(offset * Int64(sysconf(_SC_PAGESIZE)))) +} + +internal func system_munmap(_ addr: UnsafeMutableRawPointer, _ length: Int) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { + return _mock(addr, length) + } + #endif + return munmap(addr, length) +} + +internal func system_msync(_ addr: UnsafeMutableRawPointer, _ length: Int, _ flags: Int32) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { + return _mock(addr, length, flags) + } + #endif + return msync(addr, length, flags) +} +#endif diff --git a/Sources/System/Util.swift b/Sources/System/Util.swift index c038d461..a727f585 100644 --- a/Sources/System/Util.swift +++ b/Sources/System/Util.swift @@ -14,6 +14,11 @@ private func valueOrErrno( ) -> Result { i == -1 ? .failure(Errno.current) : .success(i) } +private func valueOrErrno( + valueOnFail: T, _ i: T +) -> Result { + i == valueOnFail ? .failure(Errno.current) : .success(i) +} // @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) private func nothingOrErrno( @@ -36,6 +41,21 @@ internal func valueOrErrno( } while true } +internal func valueOrErrno( + valueOnFail: T, + retryOnInterrupt: Bool, + _ f: () -> T +) -> Result { + repeat { + switch valueOrErrno(valueOnFail: valueOnFail, f()) { + case .success(let r): return .success(r) + case .failure(let err): + guard retryOnInterrupt && err == .interrupted else { return .failure(err) } + break + } + } while true +} + // @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) internal func nothingOrErrno( retryOnInterrupt: Bool, _ f: () -> I diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index a65301df..4ca4e89e 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -81,6 +81,42 @@ final class FileOperationsTest: XCTestCase { _ = try fd.duplicate(as: FileDescriptor(rawValue: 42), retryOnInterrupt: retryOnInterrupt) }, + + MockTestCase(name: "mmap", .noInterrupt, rawFD, 1, PROT_NONE, MAP_SHARED, 0) { _ in + _ = try fd.memoryMap(length: 1, pageOffset: 0, kind: .shared, protection: [.none]) + }, + + MockTestCase(name: "mmap", .noInterrupt, rawFD, 2, PROT_READ, MAP_PRIVATE, 1) { _ in + _ = try fd.memoryMap(length: 2, pageOffset: 1, kind: .private, protection: [.read]) + }, + + MockTestCase(name: "mmap", .noInterrupt, rawFD, 2, PROT_READ | PROT_WRITE, MAP_PRIVATE, 1) { _ in + _ = try fd.memoryMap(length: 2, pageOffset: 1, kind: .private, protection: [.read, .write]) + }, + + MockTestCase(name: "mmap", .noInterrupt, rawFD, 0, PROT_WRITE, MAP_PRIVATE, 1) { _ in + _ = try fd.memoryMap(length: 0, pageOffset: 1, kind: .private, protection: [.write]) + }, + + MockTestCase(name: "munmap", .noInterrupt, rawBuf.baseAddress!, 42) { _ in + _ = try fd.memoryUnmap(memoryMap: rawBuf.baseAddress!, length: 42) + }, + + MockTestCase(name: "msync", .noInterrupt, rawBuf.baseAddress!, 42, MS_SYNC) { _ in + _ = try fd.memorySync(memoryMap: rawBuf.baseAddress!, length: 42, kind: .synchronous) + }, + + MockTestCase(name: "msync", .noInterrupt, rawBuf.baseAddress!, 42, MS_ASYNC) { _ in + _ = try fd.memorySync(memoryMap: rawBuf.baseAddress!, length: 42, kind: .asynchronous) + }, + + MockTestCase(name: "msync", .noInterrupt, rawBuf.baseAddress!, 42, MS_ASYNC & MS_INVALIDATE) { _ in + _ = try fd.memorySync(memoryMap: rawBuf.baseAddress!, length: 42, kind: .asynchronous, invalidateOtherMappings: true) + }, + + MockTestCase(name: "msync", .noInterrupt, rawBuf.baseAddress!, 42, MS_SYNC & MS_INVALIDATE) { _ in + _ = try fd.memorySync(memoryMap: rawBuf.baseAddress!, length: 42, kind: .synchronous, invalidateOtherMappings: true) + }, ] for test in syscallTestCases { test.runAllTests() } @@ -160,5 +196,79 @@ final class FileOperationsTest: XCTestCase { issue26.runAllTests() } + + func testAdHocMmap() throws { + // Ad-hoc test for memory mapping a file. + let header = "swift" + let footer = "system" + do { + let fd = try FileDescriptor.open("/tmp/c.txt", + .readWrite, + options: [.create, .truncate], + permissions: .ownerReadWrite) + + try fd.closeAfter { + // Write two pages of nil bytes to the file + try fd.writeAll(Data(count: sysconf(_SC_PAGESIZE) * 2)) + + // Map the first page with write protections + let ptr1page1 = try fd.memoryMap(length: header.count, pageOffset: 0, kind: .shared, protection: [.write]) + let page1 = ptr1page1.assumingMemoryBound(to: String.self) + page1.pointee = header + + // Create another map to page1 + let ptr2page1 = try fd.memoryMap(length: header.count, pageOffset: 0, kind: .shared, protection: [.write]) + XCTAssertEqual(ptr2page1.assumingMemoryBound(to: String.self).pointee, header) + + // Map the second page with write protections + let ptr1page2 = try fd.memoryMap(length: footer.count, pageOffset: 1, kind: .shared, protection: [.write]) + let page2 = ptr1page2.assumingMemoryBound(to: String.self) + page2.pointee = footer + + // Create another map to page 2, asserting the data has been shared across the map + let ptr2page2 = try fd.memoryMap(length: footer.count, pageOffset: 1, kind: .shared, protection: [.write]) + XCTAssertEqual(ptr2page2.assumingMemoryBound(to: String.self).pointee, footer) + + // Create a *private* mapping to page 2 + let ptr2page2private = try fd.memoryMap(length: footer.count, + pageOffset: 1, + kind: .private, + protection: [.write]) + // Write to the private mapping so that the header is in place of the footer + let page2private = ptr2page2private.assumingMemoryBound(to: String.self) + page2private.pointee = header + + // Assert that the shared mappings are still correct and unaffected by the private mapping + XCTAssertEqual(ptr2page1.assumingMemoryBound(to: String.self).pointee, header) + XCTAssertEqual(ptr2page2.assumingMemoryBound(to: String.self).pointee, footer) + + // Flush changes to the filesystem + try fd.memorySync(memoryMap: ptr1page1, length: header.count, kind: .synchronous) + try fd.memorySync(memoryMap: ptr1page2, length: footer.count, kind: .synchronous) + + // Seek to the start of the file, as writing to it will have moved our offset + try fd.seek(offset: 0, from: .start) + let readBytes = try Array(unsafeUninitializedCapacity: sysconf(_SC_PAGESIZE) * 2) { (buf, count) in + count = try fd.read(into: UnsafeMutableRawBufferPointer(buf)) + } + // Assert the header and footer are correctly in place + XCTAssertEqual(String(validatingPlatformString: readBytes), header) + let readFooter = [CChar](readBytes[sysconf(_SC_PAGESIZE)..