From 3f5539a00462b18642f7be051651d67b80c77122 Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Tue, 21 Jul 2026 11:25:08 +0200 Subject: [PATCH] feat: Enable WebAssembly compilation Add wasm (wasm32-unknown-wasip1) support: depend on upstream apple/swift-nio plus the standalone PassiveLogic/nio-async-runtime, importing NIOAsyncRuntime's NIOThreadPool and MultiThreadedEventLoopGroup on wasi and NIOPosix elsewhere, gated on os(WASI). SQLiteData now stores INTEGER affinity as a SQLiteInt64 (Int64 on 32-bit) so 64-bit SQLite integers do not truncate on wasm32. NIOPosix is gated to non-wasi in the manifest; non-wasm builds are unchanged. Co-authored-by: Scott Marchant --- .github/workflows/test.yml | 2 +- Package.swift | 18 +++++++++-- Sources/SQLiteNIO/Exports.swift | 16 ++++++++++ Sources/SQLiteNIO/SQLiteConnection.swift | 14 +++++++++ Sources/SQLiteNIO/SQLiteData.swift | 30 ++++++++++++------- Sources/SQLiteNIO/SQLiteDatabase.swift | 1 - Sources/SQLiteNIO/SQLiteStatement.swift | 2 +- .../SQLiteCustomFunctionTests.swift | 2 +- Tests/SQLiteNIOTests/SQLiteNIOTests.swift | 8 ++--- 9 files changed, 71 insertions(+), 22 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 48b2792..0b32b1d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,6 +43,7 @@ jobs: ios_scheme_name: sqlite-nio with_musl: true with_android: true + with_wasm: true submit-dependencies: permissions: @@ -50,4 +51,3 @@ jobs: if: ${{ github.event_name == 'push' }} uses: vapor/ci/.github/workflows/submit-deps.yml@main secrets: inherit - diff --git a/Package.swift b/Package.swift index 3605178..cb9b6dd 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,12 @@ // swift-tools-version:5.10 import PackageDescription +/// This list matches the [supported platforms on the Swift 5.10 release of SPM](https://github.com/swiftlang/swift-package-manager/blob/release/5.10/Sources/PackageDescription/SupportedPlatforms.swift#L34-L71) +/// Don't add new platforms here unless raising the swift-tools-version of this manifest. +let allPlatforms: [Platform] = [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS, .visionOS, .driverKit, .linux, .windows, .android, .wasi, .openbsd] +let nonWASIPlatforms: [Platform] = allPlatforms.filter { $0 != .wasi } +let wasiPlatform: [Platform] = [.wasi] + let package = Package( name: "sqlite-nio", platforms: [ @@ -13,7 +19,9 @@ let package = Package( .library(name: "SQLiteNIO", targets: ["SQLiteNIO"]), ], dependencies: [ + // WASM: upstream swift-nio (NIOCore is wasm-safe) + standalone NIOAsyncRuntime for wasi. .package(url: "https://github.com/apple/swift-nio.git", from: "2.97.1"), + .package(url: "https://github.com/PassiveLogic/nio-async-runtime.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.11.0"), ], targets: [ @@ -38,7 +46,8 @@ let package = Package( .target(name: "VaporCSQLite"), .product(name: "Logging", package: "swift-log"), .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "NIOAsyncRuntime", package: "nio-async-runtime", condition: .when(platforms: wasiPlatform)), + .product(name: "NIOPosix", package: "swift-nio", condition: .when(platforms: nonWASIPlatforms)), .product(name: "NIOFoundationCompat", package: "swift-nio"), ], swiftSettings: swiftSettings @@ -92,7 +101,12 @@ var sqliteCSettings: [CSetting] { [ .define("SQLITE_OMIT_TCL_VARIABLE"), .define("SQLITE_OMIT_TRACE"), .define("SQLITE_SECURE_DELETE"), - .define("SQLITE_THREADSAFE", to: "1"), + .define("SQLITE_THREADSAFE", to: "1", .when(platforms: nonWASIPlatforms)), + // For now, we use the single threaded sqlite variation for the WASI platform + // since single-threaded operation is the least common denominator capability + // for Wasm executables and it is considered unreliable to use canImport(wasi_pthread) + // in a manifest file to distinguish between the two capabilities. + .define("SQLITE_THREADSAFE", to: "0", .when(platforms: wasiPlatform)), .define("SQLITE_UNTESTABLE"), .define("SQLITE_USE_URI"), .define("HAVE_GETHOSTUUID", to: "0", .when(platforms: [.iOS])) // silences compiler warning diff --git a/Sources/SQLiteNIO/Exports.swift b/Sources/SQLiteNIO/Exports.swift index 0542622..240a789 100644 --- a/Sources/SQLiteNIO/Exports.swift +++ b/Sources/SQLiteNIO/Exports.swift @@ -1,5 +1,21 @@ @_documentation(visibility: internal) @_exported import struct NIOCore.ByteBuffer + +// See SQLiteConnection.swift for why we gate on `os(WASI)` rather than +// `canImport(...)`: NIOAsyncRuntime is too greedy on macOS/Linux (its types +// require macOS 15+ and break older deployment targets), and NIOPosix is too +// greedy on WASI (the module imports as a partial stub but doesn't expose +// `MultiThreadedEventLoopGroup` / `NIOThreadPool` there). +#if os(WASI) +@_documentation(visibility: internal) @_exported import class NIOAsyncRuntime.NIOThreadPool +#else @_documentation(visibility: internal) @_exported import class NIOPosix.NIOThreadPool +#endif + @_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoop @_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoopGroup + +#if os(WASI) +@_documentation(visibility: internal) @_exported import class NIOAsyncRuntime.MultiThreadedEventLoopGroup +#else @_documentation(visibility: internal) @_exported import class NIOPosix.MultiThreadedEventLoopGroup +#endif diff --git a/Sources/SQLiteNIO/SQLiteConnection.swift b/Sources/SQLiteNIO/SQLiteConnection.swift index bdd02fe..2a51f17 100644 --- a/Sources/SQLiteNIO/SQLiteConnection.swift +++ b/Sources/SQLiteNIO/SQLiteConnection.swift @@ -1,6 +1,20 @@ import NIOConcurrencyHelpers import NIOCore +// Use NIOPosix on every host platform that supports it (macOS, Linux, etc.) +// and substitute NIOAsyncRuntime on WASI, where NIOPosix's `Posix*` +// primitives can't compile. Static `os(WASI)` is the right gate here: +// - `canImport(NIOAsyncRuntime)` is too greedy: NIOAsyncRuntime may sit in +// the dep graph on macOS / Linux too, but its types are +// `@available(macOS 15, *)` and break consumers with older deployment +// targets (e.g. sqlite-nio's `.macOS(.v10_15)`). +// - `canImport(NIOPosix)` is too greedy in the other direction: NIOPosix +// resolves on WASI as a partial module (no `MultiThreadedEventLoopGroup` +// / `NIOThreadPool`), so the imports below would link-fail. +#if os(WASI) +import NIOAsyncRuntime +#else import NIOPosix +#endif import VaporCSQLite import Logging diff --git a/Sources/SQLiteNIO/SQLiteData.swift b/Sources/SQLiteNIO/SQLiteData.swift index 7e3f29b..f18ced0 100644 --- a/Sources/SQLiteNIO/SQLiteData.swift +++ b/Sources/SQLiteNIO/SQLiteData.swift @@ -1,13 +1,25 @@ import VaporCSQLite import NIOCore +#if _pointerBitWidth(_64) +/// We use `Int` on 64-bit systems due to public API breakage concerns. +public typealias SQLiteInt64 = Int // 64-bit platform, Int = 64 bits +#elseif _pointerBitWidth(_32) +public typealias SQLiteInt64 = Int64 // On 32-bit platforms, we want to use 64 bit integers. +#else +/// If you hit errors here, you may simply need to add a new architectural bit size above (e.g. _128) +/// when that exists. Or, if [this proposal for pointerBitWidth](https://forums.swift.org/t/pitch-pointer-bit-width-compile-time-conditional/59572) +/// ever lands in a published Swift version, then the above conditionals may need to be adjusted: +#error("Unsupported integer size") +#endif + /// Encapsulates a single data item provided by or to SQLite. /// /// SQLite supports four data type "affinities" - INTEGER, REAL, TEXT, and BLOB - plus the `NULL` value, which has no /// innate affinity. public enum SQLiteData: Equatable, Encodable, CustomStringConvertible, Sendable { /// `INTEGER` affinity, represented in Swift by `Int`. - case integer(Int) + case integer(SQLiteInt64) /// `REAL` affinity, represented in Swift by `Double`. case float(Double) @@ -25,16 +37,12 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible, Sendable /// /// If the data has `REAL` or `TEXT` affinity, an attempt is made to interpret the value as an integer. `BLOB` /// and `NULL` values always return `nil`. - public var integer: Int? { + public var integer: SQLiteInt64? { switch self { - case .integer(let integer): - return integer - case .float(let double): - return Int(double) - case .text(let string): - return Int(string) - case .blob, .null: - return nil + case .integer(let integer): integer + case .float(let double): .init(double) + case .text(let string): .init(string) + case .blob, .null: nil } } @@ -137,7 +145,7 @@ extension SQLiteData { case SQLITE_NULL: self = .null case SQLITE_INTEGER: - self = .integer(Int(sqlite_nio_sqlite3_value_int64(sqliteValue))) + self = .integer(.init(sqlite_nio_sqlite3_value_int64(sqliteValue))) case SQLITE_FLOAT: self = .float(sqlite_nio_sqlite3_value_double(sqliteValue)) case SQLITE_TEXT: diff --git a/Sources/SQLiteNIO/SQLiteDatabase.swift b/Sources/SQLiteNIO/SQLiteDatabase.swift index 929a75b..aee7642 100644 --- a/Sources/SQLiteNIO/SQLiteDatabase.swift +++ b/Sources/SQLiteNIO/SQLiteDatabase.swift @@ -1,5 +1,4 @@ import NIOCore -import NIOPosix import VaporCSQLite import Logging diff --git a/Sources/SQLiteNIO/SQLiteStatement.swift b/Sources/SQLiteNIO/SQLiteStatement.swift index 4b24f48..1d8432c 100644 --- a/Sources/SQLiteNIO/SQLiteStatement.swift +++ b/Sources/SQLiteNIO/SQLiteStatement.swift @@ -92,7 +92,7 @@ struct SQLiteStatement { private func data(at offset: Int32) throws -> SQLiteData { switch sqlite_nio_sqlite3_column_type(self.handle, offset) { case SQLITE_INTEGER: - return .integer(Int(sqlite_nio_sqlite3_column_int64(self.handle, offset))) + return .integer(.init(sqlite_nio_sqlite3_column_int64(self.handle, offset))) case SQLITE_FLOAT: return .float(Double(sqlite_nio_sqlite3_column_double(self.handle, offset))) case SQLITE_TEXT: diff --git a/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift b/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift index 0ca82bf..24dadc9 100644 --- a/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift +++ b/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift @@ -46,7 +46,7 @@ final class DatabaseFunctionTests: XCTestCase { try await withOpenedConnection { conn in let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in 1 } try await conn.install(customFunction: fn) - await XCTAssertEqualAsync(Int(1), try await conn.query("SELECT f() as result").first?.column("result")?.integer) + await XCTAssertEqualAsync(.init(1), try await conn.query("SELECT f() as result").first?.column("result")?.integer) } } diff --git a/Tests/SQLiteNIOTests/SQLiteNIOTests.swift b/Tests/SQLiteNIOTests/SQLiteNIOTests.swift index 602ef90..6419ff5 100644 --- a/Tests/SQLiteNIOTests/SQLiteNIOTests.swift +++ b/Tests/SQLiteNIOTests/SQLiteNIOTests.swift @@ -1,8 +1,6 @@ import XCTest import SQLiteNIO import Logging -import NIOCore -import NIOPosix import NIOFoundationCompat /// Run the provided closure with an opened ``SQLiteConnection`` using an in-memory database and the singleton thread @@ -100,7 +98,7 @@ final class SQLiteNIOTests: XCTestCase { _ = try await conn.query(#"INSERT INTO test (date) VALUES (?)"#, [date.sqliteData!]) let rows = try await conn.query("SELECT * FROM test") - XCTAssertTrue(rows.first?.column("date") == .float(date.timeIntervalSince1970) || rows.first?.column("date") == .integer(Int(date.timeIntervalSince1970))) + XCTAssertTrue(rows.first?.column("date") == .float(date.timeIntervalSince1970) || rows.first?.column("date") == .integer(.init(date.timeIntervalSince1970))) XCTAssertEqual(rows.first?.column("date").flatMap(Date.init(sqliteData:))?.description, date.description) } } @@ -109,7 +107,7 @@ final class SQLiteNIOTests: XCTestCase { try await withOpenedConnection { conn in let rows = try await conn.query("SELECT 1 as foo, 2 as foo") let row0 = try XCTUnwrap(rows.first) - var i = 0 + var i: SQLiteInt64 = 0 for column in row0.columns { XCTAssertEqual(column.name, "foo") i += column.data.integer ?? 0 @@ -127,7 +125,7 @@ final class SQLiteNIOTests: XCTestCase { _ = try await conn.query(#"INSERT INTO scores (score) VALUES (?), (?), (?)"#, [.integer(3), .integer(4), .integer(5)]) struct MyAggregate: SQLiteCustomAggregate { - var sum: Int = 0 + var sum: SQLiteInt64 = 0 mutating func step(_ values: [SQLiteData]) throws { self.sum += (values.first?.integer ?? 0) }