Skip to content

Commit 2f26742

Browse files
committed
First import
1 parent 5348373 commit 2f26742

File tree

4 files changed

+300
-0
lines changed

4 files changed

+300
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,4 @@ fastlane/report.xml
6060
fastlane/Preview.html
6161
fastlane/screenshots/**/*.png
6262
fastlane/test_output
63+
.DS_Store

Package.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// swift-tools-version: 6.0
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "MemoryMap",
8+
products: [
9+
// Products define the executables and libraries a package produces, making them visible to other packages.
10+
.library(
11+
name: "MemoryMap",
12+
targets: ["MemoryMap"]),
13+
],
14+
targets: [
15+
// Targets are the basic building blocks of a package, defining a module or a test suite.
16+
// Targets can depend on other targets in this package and products from dependencies.
17+
.target(
18+
name: "MemoryMap"),
19+
.testTarget(
20+
name: "MemoryMapTests",
21+
dependencies: ["MemoryMap"]
22+
),
23+
]
24+
)

Sources/MemoryMap/MemoryMap.swift

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import Foundation
2+
import Darwin
3+
import os
4+
5+
/// MemoryMap is a utility class that backs a Plain Old Data (POD) struct
6+
/// with a memory-mapped file. This enables efficient persistence and
7+
/// crash-resilient storage, with thread-safe access.
8+
public class MemoryMap<T>: @unchecked Sendable {
9+
10+
/// The URL of the memory-mapped file.
11+
public let url: URL
12+
13+
/// Initializes a memory-mapped file for the given POD type `T`.
14+
///
15+
/// If the file does not exist, it will be created. If the file is smaller
16+
/// than the required size (`MemoryLayout<T>` + a magic number), it will be resized.
17+
/// A magic number is stored and validated to ensure data integrity.
18+
///
19+
/// - Parameters:
20+
/// - fileURL: The file's location on disk.
21+
///
22+
/// Note: `T` must be a Plain Old Data (POD) type. This is validated at runtime.
23+
public init(fileURL: URL) throws {
24+
25+
// only POD types are allowed, so basically
26+
// structs with built-in types (aka. trivial).
27+
assert(_isPOD(T.self), "\(type(of: T.self)) is a non-trivial Type.")
28+
29+
// Ensure we're not creating a huge file.
30+
// 1MB for PODs should be plenty.
31+
let maxSize = 1024 * 1024
32+
guard MemoryLayout<MemoryMapContainer>.stride <= maxSize else {
33+
throw MemoryMapError.invalidSize
34+
}
35+
36+
self.url = fileURL
37+
self.container = try Self._mmap(self.url, size: MemoryLayout<MemoryMapContainer>.stride)
38+
self._s = withUnsafeMutablePointer(to: &self.container.pointee._s) {
39+
UnsafeMutablePointer($0)
40+
}
41+
}
42+
43+
/// Provides thread-safe access to the mapped data.
44+
///
45+
/// This property allows reading and writing of the POD struct `T`.
46+
/// Changes are immediately reflected in the memory-mapped file.
47+
public var get: UnsafeMutablePointer<T>.Pointee {
48+
get {
49+
lock.lock()
50+
defer { lock.unlock() }
51+
return self._s.pointee
52+
}
53+
set {
54+
lock.lock()
55+
defer { lock.unlock() }
56+
self._s.pointee = newValue
57+
}
58+
}
59+
60+
// MARK: - private
61+
62+
deinit {
63+
munmap(self.container, MemoryLayout<MemoryMapContainer>.stride)
64+
}
65+
66+
/// Maps the specified file to memory, creating or resizing it as necessary.
67+
///
68+
/// - Parameters:
69+
/// - fileURL: The file's location on disk.
70+
/// - size: The size of the memory-mapped region.
71+
///
72+
/// - Returns: A pointer to the mapped memory.
73+
///
74+
private static func _mmap(_ fileURL: URL, size: Int) throws -> UnsafeMutablePointer<MemoryMapContainer> {
75+
76+
let fileExists = FileManager.default.fileExists(atPath: fileURL.path)
77+
78+
// open and ensure we create if non-existant.
79+
// close on end of scope
80+
let fd = open(fileURL.path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR)
81+
guard fd > 0 else {
82+
throw MemoryMapError.unix(errno, "open", fileURL)
83+
}
84+
defer { close(fd) }
85+
86+
// Get the file size
87+
var stat = stat()
88+
guard fstat(fd, &stat) == 0 else {
89+
throw MemoryMapError.unix(errno, "fstat", fileURL)
90+
}
91+
92+
// resize if needed
93+
if stat.st_size < size {
94+
guard ftruncate(fd, off_t(size)) == 0 else {
95+
throw MemoryMapError.unix(errno, "ftruncate", fileURL)
96+
}
97+
}
98+
99+
// map it
100+
let map = mmap(nil, size, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0)
101+
guard map != MAP_FAILED else {
102+
throw MemoryMapError.unix(errno, "mmap", fileURL)
103+
}
104+
105+
// Unmap when we defer.
106+
// if any early returns get added, an error
107+
// will occur if _unmapOnDefer_ isn't set.
108+
let unmapOnDefer: Bool
109+
defer { if unmapOnDefer { munmap(map, size) } }
110+
111+
guard Int(bitPattern: map) % MemoryLayout<MemoryMapContainer>.alignment == 0 else {
112+
unmapOnDefer = true
113+
throw MemoryMapError.alignment
114+
}
115+
116+
guard let pointer = map?.bindMemory(to: MemoryMapContainer.self, capacity: 1) else {
117+
unmapOnDefer = true
118+
throw MemoryMapError.failedBind
119+
}
120+
121+
// This is our default magic number that
122+
// should be at the top of every container.
123+
let defaultMagic: UInt64 = 0xB10C
124+
125+
// If the file doesn't exists, set it up with defaults
126+
if !fileExists {
127+
pointer.pointee.magic = defaultMagic
128+
}
129+
130+
// ensure magic
131+
guard pointer.pointee.magic == defaultMagic else {
132+
unmapOnDefer = true
133+
throw MemoryMapError.notMemoryMap
134+
}
135+
136+
unmapOnDefer = false
137+
return pointer
138+
}
139+
140+
// Swift doesn't have any struct packing.
141+
// What it does do however is take the largest member
142+
// and use that for alignment.
143+
// So in theory, if we start a struct with 64 bits,
144+
// each member should be padded to 64bit (.alignment).
145+
private struct MemoryMapContainer {
146+
var magic: UInt64
147+
var _s: T
148+
}
149+
150+
private let lock = NSLock()
151+
private let container: UnsafeMutablePointer<MemoryMapContainer>
152+
private let _s: UnsafeMutablePointer<T>
153+
}
154+
155+
public enum MemoryMapError: Error {
156+
157+
/// A unix error of some sort (open, mmap, ...).
158+
case unix(Int32, String, URL)
159+
160+
/// Memory layout alignment is incorrect.
161+
case alignment
162+
163+
/// `.bindMemory` failed.
164+
case failedBind
165+
166+
/// The header magic number is wrong.
167+
case notMemoryMap
168+
169+
/// The struct backing the map is too big.
170+
case invalidSize
171+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import XCTest
2+
@testable import MemoryMap
3+
4+
struct MemoryMapTestPOD {
5+
var state: Int8
6+
var ok: Bool
7+
var size: Int64
8+
}
9+
10+
struct TestStruct: Equatable {
11+
var intValue: Int
12+
var doubleValue: Double
13+
}
14+
15+
final class MemoryMapTests: XCTestCase {
16+
17+
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
18+
19+
override func tearDown() {
20+
try? FileManager.default.removeItem(at: url)
21+
}
22+
23+
func testWriteCloseVerify() throws {
24+
25+
var map: MemoryMap<MemoryMapTestPOD>? = try? MemoryMap(fileURL: url)
26+
XCTAssertNotNil(map)
27+
28+
XCTAssertEqual(map?.get.state, 0)
29+
XCTAssertEqual(map?.get.ok, false)
30+
XCTAssertEqual(map?.get.size, 0)
31+
32+
map?.get.state = 10
33+
map?.get.ok = true
34+
map?.get.size = 50
35+
36+
XCTAssertEqual(map?.get.state, 10)
37+
XCTAssertEqual(map?.get.ok, true)
38+
XCTAssertEqual(map?.get.size, 50)
39+
40+
map = try? MemoryMap(fileURL: url)
41+
XCTAssertNotNil(map)
42+
43+
XCTAssertEqual(map?.get.state, 10)
44+
XCTAssertEqual(map?.get.ok, true)
45+
XCTAssertEqual(map?.get.size, 50)
46+
47+
}
48+
49+
func testInitializationAndDefaultValue() throws {
50+
let memoryMap = try MemoryMap<TestStruct>(fileURL: url)
51+
XCTAssertEqual(memoryMap.get, TestStruct(intValue: 0, doubleValue: 0.0), "Default values should be zeroed for new file.")
52+
}
53+
54+
func testWriteAndReadBack() throws {
55+
let memoryMap = try MemoryMap<TestStruct>(fileURL: url)
56+
let testValue = TestStruct(intValue: 42, doubleValue: 3.14)
57+
memoryMap.get = testValue
58+
59+
XCTAssertEqual(memoryMap.get, testValue, "Written and read-back values should match.")
60+
}
61+
62+
func testPersistenceAcrossInstances() throws {
63+
let initialValue = TestStruct(intValue: 123, doubleValue: 45.67)
64+
65+
// Write value
66+
var memoryMap = try MemoryMap<TestStruct>(fileURL: url)
67+
memoryMap.get = initialValue
68+
69+
// Read value in a new instance
70+
memoryMap = try MemoryMap<TestStruct>(fileURL: url)
71+
XCTAssertEqual(memoryMap.get, initialValue, "Data should persist across instances.")
72+
}
73+
74+
func testThreadSafety() throws {
75+
let memoryMap = try MemoryMap<TestStruct>(fileURL: url)
76+
77+
let expectation = XCTestExpectation(description: "Concurrent writes complete")
78+
expectation.expectedFulfillmentCount = 10
79+
80+
DispatchQueue.concurrentPerform(iterations: 1000) { index in
81+
memoryMap.get = TestStruct(intValue: index, doubleValue: Double(index))
82+
expectation.fulfill()
83+
}
84+
85+
wait(for: [expectation], timeout: 5.0)
86+
// Since writes are last-wins, just check no crashes occurred and value is valid
87+
XCTAssertNotNil(memoryMap.get, "Memory map should remain valid during concurrent writes.")
88+
}
89+
90+
func testMagicNumberMismatch() throws {
91+
let fd = open(url.path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR)
92+
defer { close(fd) }
93+
94+
var invalidMagic: UInt64 = 0xDEADBEEF
95+
write(fd, &invalidMagic, MemoryLayout<UInt64>.size)
96+
97+
XCTAssertThrowsError(try MemoryMap<TestStruct>(fileURL: url)) { error in
98+
guard case MemoryMapError.notMemoryMap = error else {
99+
XCTFail("Expected MemoryMapError.notMemoryMap, got \(error)")
100+
return
101+
}
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)