Skip to content

Commit e16765f

Browse files
WasmParser: Stop taking FilePath in detectWasmFileType (#368)
The caller usually uses a `FileDescriptor` to parse the file after detecting the file type, so opening fd inside the detection function is wasteful. We can instead move the fd opening to the caller. Also this reduces system dependencies in the `WasmParser` module.
1 parent 0db2c80 commit e16765f

8 files changed

Lines changed: 157 additions & 103 deletions

File tree

Package.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ let package = Package(
111111
.target(
112112
name: "WasmParser",
113113
dependencies: [
114-
"SystemExtras",
115114
"WasmTypes",
116115
.product(name: "SystemPackage", package: "swift-system"),
117116
.target(

Sources/CLICommands/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
add_wasmkit_library(CLICommands
22
Explore.swift
3+
Platform.swift
34
Run.swift
45
Wat2wasm.swift
56
)

Sources/CLICommands/Platform.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import struct SystemPackage.FileDescriptor
2+
3+
#if os(Windows)
4+
import ucrt
5+
#endif
6+
7+
enum Platform {
8+
#if os(Windows)
9+
// TODO: Upstream `O_BINARY` to `SystemPackage
10+
static let readOnlyBinaryAccessMode = FileDescriptor.AccessMode(
11+
rawValue: FileDescriptor.AccessMode.readOnly.rawValue | O_BINARY
12+
)
13+
static let readOnlyTextAccessMode = FileDescriptor.AccessMode(
14+
rawValue: FileDescriptor.AccessMode.readOnly.rawValue | O_TEXT
15+
)
16+
#else
17+
static let readOnlyBinaryAccessMode: FileDescriptor.AccessMode = .readOnly
18+
static let readOnlyTextAccessMode: FileDescriptor.AccessMode = .readOnly
19+
#endif
20+
}

Sources/CLICommands/Run.swift

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -159,27 +159,70 @@ package struct Run: AsyncParsableCommand {
159159

160160
log("Started parsing module", verbose: true)
161161

162-
// Detect file type (component vs module)
163162
let filePath = FilePath(path)
164-
let fileType = try detectWasmFileType(filePath: filePath)
163+
let module: Module
165164

166-
#if ComponentModel
167-
if fileType == .component {
168-
try runComponent(filePath: filePath)
169-
return
165+
if filePath.extension == "wat" {
166+
let fileHandle: FileDescriptor = try FileDescriptor.open(filePath, Platform.readOnlyTextAccessMode)
167+
module = try withThrowing {
168+
let size = try fileHandle.seek(offset: 0, from: .end)
169+
_ = try fileHandle.seek(offset: 0, from: .start)
170+
let wat = try String(unsafeUninitializedCapacity: Int(size)) {
171+
try fileHandle.read(into: UnsafeMutableRawBufferPointer($0))
172+
}
173+
return try WasmKit.parseWasm(bytes: wat2wasm(wat))
174+
} defer: {
175+
try fileHandle.close()
170176
}
171-
#endif
177+
} else {
178+
let fileHandle: FileDescriptor = try FileDescriptor.open(filePath, Platform.readOnlyBinaryAccessMode)
179+
#if ComponentModel
180+
var parsedComponent: ParsedComponent?
181+
#endif
182+
let parsedModule: Module? = try withThrowing { () throws -> Module? in
183+
var magic: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0, 0, 0, 0)
184+
let bytesRead = try withUnsafeMutableBytes(of: &magic) { buffer in
185+
try fileHandle.read(into: buffer)
186+
}
172187

173-
// Regular module execution
174-
let module: Module
175-
if verbose, #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
176-
let (parsedModule, parseTime) = try measure {
177-
try parseWasm(filePath: filePath)
188+
// Detect file type (component vs module)
189+
let fileType: WasmFileType
190+
if bytesRead == MemoryLayout.size(ofValue: magic) {
191+
fileType = detectWasmFileType(magic)
192+
} else {
193+
fileType = .unknown
194+
}
195+
196+
#if ComponentModel
197+
if fileType == .component {
198+
log("Detected component binary, parsing component...", verbose: true)
199+
_ = try fileHandle.seek(offset: 0, from: .start)
200+
parsedComponent = try parseComponent(fileHandle: fileHandle)
201+
return nil
202+
}
203+
#endif
204+
205+
guard fileType == .coreModule else {
206+
fatalError("Unsupported WebAssembly file type: \(fileType)")
207+
}
208+
209+
_ = try fileHandle.seek(offset: 0, from: .start)
210+
let (parsedModule, parseTime) = try measure {
211+
try WasmKit.parseWasm(fileHandle: fileHandle)
212+
}
213+
log("Finished parsing module: \(parseTime)", verbose: true)
214+
return parsedModule
215+
} defer: {
216+
try fileHandle.close()
178217
}
179-
log("Finished parsing module: \(parseTime)", verbose: true)
218+
#if ComponentModel
219+
if let parsedComponent {
220+
try runComponent(component: parsedComponent)
221+
return
222+
}
223+
#endif
224+
guard let parsedModule else { return }
180225
module = parsedModule
181-
} else {
182-
module = try parseWasm(filePath: filePath)
183226
}
184227

185228
let (interceptor, finalize) = try deriveInterceptor()
@@ -205,11 +248,7 @@ package struct Run: AsyncParsableCommand {
205248

206249
#if ComponentModel
207250
/// Run a WebAssembly component.
208-
func runComponent(filePath: FilePath) throws {
209-
log("Detected component binary, parsing component...", verbose: true)
210-
211-
let component = try parseComponent(filePath: filePath)
212-
251+
func runComponent(component: ParsedComponent) throws {
213252
let engine = Engine(configuration: deriveRuntimeConfiguration())
214253
let store = Store(engine: engine)
215254
let loader = ComponentLoader(store: store)
@@ -350,25 +389,6 @@ package struct Run: AsyncParsableCommand {
350389
}
351390
}
352391

353-
/// Parses a `.wasm` or `.wat` module.
354-
func parseWasm(filePath: FilePath) throws -> Module {
355-
if filePath.extension == "wat", #available(macOS 11.0, iOS 14.0, macCatalyst 14.0, tvOS 14.0, visionOS 1.0, watchOS 7.0, *) {
356-
let fileHandle = try FileDescriptor.open(filePath, .readOnly)
357-
return try withThrowing {
358-
let size = try fileHandle.seek(offset: 0, from: .end)
359-
360-
let wat = try String(unsafeUninitializedCapacity: Int(size)) {
361-
try fileHandle.read(fromAbsoluteOffset: 0, into: .init($0))
362-
}
363-
return try WasmKit.parseWasm(bytes: wat2wasm(wat))
364-
} defer: {
365-
try fileHandle.close()
366-
}
367-
} else {
368-
return try WasmKit.parseWasm(filePath: filePath)
369-
}
370-
}
371-
372392
extension Run {
373393
package static func parseInvocation(arguments: [String]) -> (functionName: String?, parameters: [Value]) {
374394
let functionName = arguments.first

Sources/WasmKit/Execution/ParsedComponentBuilder.swift

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,20 @@
11
#if ComponentModel
22
import ComponentModel
3-
import SystemExtras
43
import SystemPackage
54
import WasmParser
65

76
// MARK: - Component Parsing
87

9-
/// Parse a component binary file into a `ParsedComponent` ready for instantiation.
8+
/// Parse a component binary file from a caller-owned file descriptor.
109
///
11-
/// This function reads the component from a file using streaming I/O for efficiency.
12-
///
13-
/// - Parameters:
14-
/// - filePath: Path to the WebAssembly component binary file
15-
/// - features: Enabled WebAssembly features for parsing
16-
/// - Returns: A `ParsedComponent` ready for instantiation
17-
/// - Throws: `WasmKitError` if parsing fails, `ComponentParseError` for semantic errors
10+
/// The descriptor is consumed from its current offset and is not closed by
11+
/// this function.
1812
public func parseComponent(
19-
filePath: FilePath,
13+
fileHandle: FileDescriptor,
2014
features: WasmFeatureSet = .default
2115
) throws -> ParsedComponent {
22-
let fileHandle = try FileDescriptor.open(filePath, .readOnly)
23-
return try withThrowing {
24-
let stream = try FileHandleStream(fileHandle: fileHandle)
25-
return try parseComponent(stream: stream, features: features)
26-
} defer: {
27-
try fileHandle.close()
28-
}
16+
let stream = try FileHandleStream(fileHandle: fileHandle)
17+
return try parseComponent(stream: stream, features: features)
2918
}
3019

3120
/// Parse a component binary into a `ParsedComponent` ready for instantiation.

Sources/WasmKit/ModuleParser.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,22 @@ public func parseWasm(filePath: FilePath, features: WasmFeatureSet = .default) t
1919
#endif
2020
let fileHandle = try FileDescriptor.open(filePath, accessMode)
2121
return try withThrowing {
22-
let stream = try FileHandleStream(fileHandle: fileHandle)
23-
let module = try parseModule(stream: stream, features: features)
24-
return module
22+
try parseWasm(fileHandle: fileHandle, features: features)
2523
} defer: {
2624
try fileHandle.close()
2725
}
2826
}
2927

28+
/// Parse a WebAssembly binary file from a caller-owned file descriptor.
29+
///
30+
/// The descriptor is consumed from its current offset and is not closed by
31+
/// this function.
32+
public func parseWasm(fileHandle: FileDescriptor, features: WasmFeatureSet = .default) throws -> Module {
33+
let stream = try FileHandleStream(fileHandle: fileHandle)
34+
let module = try parseModule(stream: stream, features: features)
35+
return module
36+
}
37+
3038
/// Parse a given byte array as a WebAssembly binary format file
3139
/// > Note: <https://webassembly.github.io/spec/core/binary/index.html>
3240
public func parseWasm(bytes: [UInt8], features: WasmFeatureSet = .default) throws(WasmKitError) -> Module {

Sources/WasmParser/WasmParser.swift

Lines changed: 23 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import SystemExtras
21
import WasmTypes
32

43
import struct SystemPackage.FileDescriptor
@@ -1333,50 +1332,31 @@ public enum WasmFileType: Equatable, Sendable {
13331332
case unknown
13341333
}
13351334

1336-
/// Detect the type of a WebAssembly binary file by reading its header.
1335+
/// Detect the type of a WebAssembly binary file from its header.
13371336
///
1338-
/// This function reads the 8-byte WebAssembly header to determine whether
1339-
/// the file contains a core module or a component. Uses stack allocation
1340-
/// only (no heap allocation for the header bytes).
1337+
/// This function classifies the 8-byte WebAssembly header to determine
1338+
/// whether it identifies a core module or a component.
13411339
///
1342-
/// - Parameter filePath: Path to the WebAssembly binary file
1340+
/// - Parameter header: The first 8 bytes of the WebAssembly binary file.
13431341
/// - Returns: The detected file type
1344-
/// - Throws: If the file cannot be opened or read
1345-
public func detectWasmFileType(filePath: FilePath) throws -> WasmFileType {
1346-
let fileHandle = try FileDescriptor.open(filePath, .readOnly)
1347-
return try withThrowing {
1348-
// Use a tuple to avoid heap allocation - 8 bytes on stack
1349-
// TODO: needs a `SmallArray` abstraction until `InlineArray` becomes available after dropping support for macOS 15.
1350-
var header: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0, 0, 0, 0)
1351-
let bytesRead = try withUnsafeMutableBytes(of: &header) { buffer in
1352-
try fileHandle.read(into: buffer)
1353-
}
1354-
1355-
// Need at least 8 bytes for a valid header
1356-
guard bytesRead >= 8 else {
1357-
return .unknown
1358-
}
1359-
1360-
// Check magic number: \0asm (uses WASM_MAGIC as source of truth)
1361-
guard
1362-
header.0 == WASM_MAGIC[0] && header.1 == WASM_MAGIC[1]
1363-
&& header.2 == WASM_MAGIC[2] && header.3 == WASM_MAGIC[3]
1364-
else {
1365-
return .unknown
1366-
}
1367-
1368-
// Check version and layer bytes:
1369-
// - Core module: version=0x01, 0x00 and layer=0x00, 0x00
1370-
// - Component: version=0x0d, 0x00 and layer=0x01, 0x00
1371-
switch (header.4, header.5, header.6, header.7) {
1372-
case (0x01, 0x00, 0x00, 0x00):
1373-
return .coreModule
1374-
case (0x0d, 0x00, 0x01, 0x00):
1375-
return .component
1376-
default:
1377-
return .unknown
1378-
}
1379-
} defer: {
1380-
try fileHandle.close()
1342+
public func detectWasmFileType(_ header: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) -> WasmFileType {
1343+
// Check magic number: \0asm (uses WASM_MAGIC as source of truth)
1344+
guard
1345+
header.0 == WASM_MAGIC[0] && header.1 == WASM_MAGIC[1]
1346+
&& header.2 == WASM_MAGIC[2] && header.3 == WASM_MAGIC[3]
1347+
else {
1348+
return .unknown
1349+
}
1350+
1351+
// Check version and layer bytes:
1352+
// - Core module: version=0x01, 0x00 and layer=0x00, 0x00
1353+
// - Component: version=0x0d, 0x00 and layer=0x01, 0x00
1354+
switch (header.4, header.5, header.6, header.7) {
1355+
case (0x01, 0x00, 0x00, 0x00):
1356+
return .coreModule
1357+
case (0x0d, 0x00, 0x01, 0x00):
1358+
return .component
1359+
default:
1360+
return .unknown
13811361
}
13821362
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Testing
2+
3+
@testable import WasmParser
4+
5+
@Suite struct WasmFileTypeTests {
6+
@Test func detectsCoreModuleHeader() {
7+
let header: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (
8+
0x00, 0x61, 0x73, 0x6d,
9+
0x01, 0x00, 0x00, 0x00
10+
)
11+
12+
#expect(detectWasmFileType(header) == .coreModule)
13+
}
14+
15+
@Test func detectsComponentHeader() {
16+
let header: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (
17+
0x00, 0x61, 0x73, 0x6d,
18+
0x0d, 0x00, 0x01, 0x00
19+
)
20+
21+
#expect(detectWasmFileType(header) == .component)
22+
}
23+
24+
@Test func rejectsUnknownHeaders() {
25+
let invalidMagic: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (
26+
0x00, 0x61, 0x73, 0x78,
27+
0x01, 0x00, 0x00, 0x00
28+
)
29+
let invalidVersion: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (
30+
0x00, 0x61, 0x73, 0x6d,
31+
0x02, 0x00, 0x00, 0x00
32+
)
33+
34+
#expect(detectWasmFileType(invalidMagic) == .unknown)
35+
#expect(detectWasmFileType(invalidVersion) == .unknown)
36+
}
37+
}

0 commit comments

Comments
 (0)