Replace Objective-C bridge with Swift ONNX runtime backend
CodeQL Advanced / Analyze (swift) (push) Has been cancelled

This commit is contained in:
2026-06-09 09:38:43 -04:00
parent b7fb7f7dea
commit 2af97cc6bf
8 changed files with 1648 additions and 124 deletions
+25 -104
View File
@@ -1,114 +1,35 @@
// swift-tools-version: 5.9
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
// A user of the Swift Package Manager (SPM) package will consume this file directly from the ORT SPM github repository.
// For example, the end user's config will look something like:
//
// dependencies: [
// .package(url: "https://github.com/microsoft/onnxruntime-swift-package-manager", from: "1.16.0"),
// ...
// ],
//
// NOTE: For valid version numbers, please refer to this page:
// https://github.com/microsoft/onnxruntime-swift-package-manager/releases
// swift-tools-version: 6.1
import PackageDescription
import class Foundation.ProcessInfo
let package = Package(
name: "onnxruntime",
platforms: [.iOS(.v15),
.macOS(.v14)],
products: [
.library(name: "onnxruntime",
type: .static,
targets: ["OnnxRuntimeBindings"]),
.library(name: "onnxruntime_extensions",
type: .static,
targets: ["OnnxRuntimeExtensions"]),
.library(
name: "onnxruntime",
targets: ["OnnxRuntimeBindings"]
),
],
dependencies: [],
targets: [
.target(name: "OnnxRuntimeBindings",
dependencies: ["onnxruntime"],
path: "objectivec",
exclude: ["ReadMe.md", "format_objc.sh", "test", "docs",
"ort_checkpoint.mm",
"ort_checkpoint_internal.h",
"ort_training_session_internal.h",
"ort_training_session.mm",
"include/ort_checkpoint.h",
"include/ort_training_session.h",
"include/onnxruntime_training.h"],
cxxSettings: [
.define("SPM_BUILD"),
]),
.testTarget(name: "OnnxRuntimeBindingsTests",
dependencies: ["OnnxRuntimeBindings"],
path: "swift/OnnxRuntimeBindingsTests",
resources: [
.copy("Resources/single_add.basic.ort")
]),
.target(name: "OnnxRuntimeExtensions",
dependencies: ["onnxruntime_extensions", "onnxruntime"],
path: "extensions",
cxxSettings: [
.define("ORT_SWIFT_PACKAGE_MANAGER_BUILD"),
]),
.testTarget(name: "OnnxRuntimeExtensionsTests",
dependencies: ["OnnxRuntimeExtensions", "OnnxRuntimeBindings"],
path: "swift/OnnxRuntimeExtensionsTests",
resources: [
.copy("Resources/decode_image.onnx")
]),
.systemLibrary(
name: "COnnxRuntime",
path: "swift/COnnxRuntime",
pkgConfig: "libonnxruntime"
),
.target(
name: "OnnxRuntimeBindings",
dependencies: ["COnnxRuntime"],
path: "swift/OnnxRuntimeBindings"
),
.testTarget(
name: "OnnxRuntimeBindingsTests",
dependencies: ["OnnxRuntimeBindings", "COnnxRuntime"],
path: "swift/OnnxRuntimeBindingsTests",
exclude: ["SwiftOnnxRuntimeBindingsTests.swift"],
resources: [
.copy("Resources/single_add.basic.ort")
]
),
],
cxxLanguageStandard: .cxx17
cxxLanguageStandard: .cxx20
)
// Add the ORT CocoaPods C/C++ pod archive as a binary target.
//
// There are 2 scenarios:
// - Target will be set to a released pod archive and its checksum.
//
// - Target will be set to a local pod archive.
// This can be used to test with the latest (not yet released) ORT Objective-C source code.
// CI or local testing where you have built/obtained the pod archive matching the current source code.
// Requires the ORT_POD_LOCAL_PATH environment variable to be set to specify the location of the pod.
if let pod_archive_path = ProcessInfo.processInfo.environment["ORT_POD_LOCAL_PATH"] {
// ORT_POD_LOCAL_PATH MUST be a path that is relative to Package.swift.
//
// To build locally, tools/ci_build/github/apple/build_and_assemble_apple_pods.py can be used
// See https://onnxruntime.ai/docs/build/custom.html#ios
// Example command:
// python3 tools/ci_build/github/apple/build_and_assemble_apple_pods.py \
// --build-settings-file tools/ci_build/github/apple/default_full_apple_framework_build_settings.json
//
// This should produce the pod archive in build/apple_pod_staging, and ORT_POD_LOCAL_PATH can be set to
// "build/apple_pod_staging/pod-archive-onnxruntime-c-???.zip" where '???' is replaced by the version info in the
// actual filename.
package.targets.append(Target.binaryTarget(name: "onnxruntime", path: pod_archive_path))
} else {
// ORT release
package.targets.append(
Target.binaryTarget(name: "onnxruntime",
url: "https://download.onnxruntime.ai/pod-archive-onnxruntime-c-1.24.2.zip",
// SHA256 checksum
checksum: "f7100a992d2a8135168c8afd831e6a58b465349101982aa58b3e11d36e600b54")
)
}
if let ext_pod_archive_path = ProcessInfo.processInfo.environment["ORT_EXTENSIONS_POD_LOCAL_PATH"] {
package.targets.append(Target.binaryTarget(name: "onnxruntime_extensions", path: ext_pod_archive_path))
} else {
// ORT Extensions release
package.targets.append(
Target.binaryTarget(name: "onnxruntime_extensions",
url: "https://download.onnxruntime.ai/pod-archive-onnxruntime-extensions-c-0.13.0.zip",
// SHA256 checksum
checksum: "346522d1171d4c99cb0908fa8e4e9330a4a6aad39cd83ce36eb654437b33e6b5")
)
}
+36 -20
View File
@@ -1,32 +1,48 @@
# Swift Package Manager for ONNX Runtime
A light-weight repository for providing [Swift Package Manager (SPM)](https://www.swift.org/package-manager/) support for [ONNXRuntime](https://github.com/microsoft/onnxruntime). The ONNX Runtime native package is included as a binary dependency of the SPM package.
Swift-native ONNX Runtime bindings derived from the public Objective-C wrapper surface, but implemented without the Objective-C bridge so the package can build on Linux.
## Status
SPM is the alternative to CocoaPods when desired platform to consume is mobile iOS.
- Public API covers inference, session/run options, tensor values, checkpoint state, and training session entry points.
- Backend is wired to the ONNX Runtime C API and training C API through a Swift system library target.
- The package expects `libonnxruntime` and headers to be available on the host, typically via `pkg-config`.
## Note
## Requirements
The `objectivec/` directory is copied from the [ORT repo](https://github.com/microsoft/onnxruntime/tree/main/objectivec) and it's expected to match. It will be updated periodically/before release to merge new changes.
- Swift 6.1+
- ONNX Runtime installed on the system with:
- `libonnxruntime`
- `onnxruntime_c_api.h`
- `onnxruntime_training_c_api.h`
- `pkg-config` metadata for `libonnxruntime`
## Contributing
## Usage
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
```swift
import OnnxRuntimeBindings
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
let environment = try ORTEnv(loggingLevel: .warning)
let sessionOptions = try ORTSessionOptions()
let session = try ORTSession(
env: environment,
modelPath: "/path/to/model.onnx",
sessionOptions: sessionOptions
)
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
let inputTensor = try ORTValue(
tensorData: NSMutableData(data: inputData),
elementType: .float,
shape: [1, 224, 224, 3]
)
## Trademarks
let outputs = try session.run(
inputs: ["input": inputTensor],
outputNames: ["output"]
)
```
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
trademarks or logos is subject to and must follow
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
Any use of third-party trademarks or logos are subject to those third-party's policies.
## Notes
- The `objectivec/` directory remains in the repo as the reference contract surface from upstream ONNX Runtime.
- The shipped Swift package target is the pure Swift `OnnxRuntimeBindings` implementation under `swift/OnnxRuntimeBindings`.
+5
View File
@@ -0,0 +1,5 @@
module COnnxRuntime [system] {
header "shim.h"
link "onnxruntime"
export *
}
+2
View File
@@ -0,0 +1,2 @@
#include <onnxruntime/onnxruntime_c_api.h>
#include <onnxruntime/onnxruntime_training_c_api.h>
+21
View File
@@ -0,0 +1,21 @@
import Foundation
public enum ORTError: Error, LocalizedError {
case notImplemented(String)
case runtimeFailure(action: String, message: String)
case invalidArgument(String)
case unsupported(String)
public var errorDescription: String? {
switch self {
case .notImplemented(let symbol):
return "ONNX Runtime Swift interface symbol is not implemented yet: \(symbol)"
case .runtimeFailure(let action, let message):
return "\(action) failed: \(message)"
case .invalidArgument(let message):
return message
case .unsupported(let message):
return message
}
}
}
File diff suppressed because it is too large Load Diff
+166
View File
@@ -0,0 +1,166 @@
@preconcurrency import COnnxRuntime
import Foundation
internal enum ORTNative {
nonisolated(unsafe) static let apiBase = OrtGetApiBase()
nonisolated(unsafe) static let api = apiBase!.pointee.GetApi(UInt32(ORT_API_VERSION))!
nonisolated(unsafe) static let trainingApi = api.pointee.GetTrainingApi?(UInt32(ORT_API_VERSION))
static func check(_ status: OpaquePointer?, action: String) throws {
guard let status else {
return
}
let message = api.pointee.GetErrorMessage?(status).map { String(cString: $0) } ?? "Unknown ONNX Runtime error"
api.pointee.ReleaseStatus?(status)
throw ORTError.runtimeFailure(action: action, message: message)
}
static func withAllocator<Result>(_ body: (UnsafeMutablePointer<OrtAllocator>) throws -> Result) throws -> Result {
var allocator: UnsafeMutablePointer<OrtAllocator>?
try check(api.pointee.GetAllocatorWithDefaultOptions?(&allocator), action: "GetAllocatorWithDefaultOptions")
guard let allocator else {
throw ORTError.runtimeFailure(action: "GetAllocatorWithDefaultOptions", message: "Allocator was not returned")
}
return try body(allocator)
}
static func copyAllocatedString(
_ stringPointer: UnsafeMutablePointer<CChar>?,
allocator: UnsafeMutablePointer<OrtAllocator>
) throws -> String {
guard let stringPointer else {
throw ORTError.runtimeFailure(action: "copyAllocatedString", message: "Expected allocated string")
}
defer {
_ = api.pointee.AllocatorFree?(allocator, stringPointer)
}
return String(cString: stringPointer)
}
static func tensorElementCount(shape: [Int]) throws -> Int {
guard shape.allSatisfy({ $0 >= 0 }) else {
throw ORTError.invalidArgument("Tensor shape contains a negative dimension: \(shape)")
}
return shape.reduce(1, *)
}
static func elementByteCount(for type: ORTTensorElementDataType) throws -> Int {
switch type {
case .float:
return MemoryLayout<Float>.stride
case .int8:
return MemoryLayout<Int8>.stride
case .uInt8:
return MemoryLayout<UInt8>.stride
case .int32:
return MemoryLayout<Int32>.stride
case .uInt32:
return MemoryLayout<UInt32>.stride
case .int64:
return MemoryLayout<Int64>.stride
case .uInt64:
return MemoryLayout<UInt64>.stride
case .string:
throw ORTError.invalidArgument("String tensors do not have a fixed byte width")
case .undefined:
throw ORTError.invalidArgument("Undefined tensor element type is not supported")
}
}
static func makeShapeBuffer(_ shape: [Int]) -> [Int64] {
shape.map(Int64.init)
}
static func withCStringArray<Result>(
_ strings: [String],
_ body: ([UnsafePointer<CChar>?]) throws -> Result
) rethrows -> Result {
let storage = strings.map { Array($0.utf8CString) }
let pointers = storage.map { $0.withUnsafeBufferPointer { $0.baseAddress } }
return try body(pointers)
}
static func fetchTensorInfo(from value: OpaquePointer) throws -> ORTTensorTypeAndShapeInfo {
var info: OpaquePointer?
try check(api.pointee.GetTensorTypeAndShape?(value, &info), action: "GetTensorTypeAndShape")
guard let info else {
throw ORTError.runtimeFailure(action: "GetTensorTypeAndShape", message: "Tensor info was not returned")
}
defer {
api.pointee.ReleaseTensorTypeAndShapeInfo?(info)
}
var elementType = ONNXTensorElementDataType(rawValue: 0)
try check(api.pointee.GetTensorElementType?(info, &elementType), action: "GetTensorElementType")
var dimensionsCount: Int = 0
try check(api.pointee.GetDimensionsCount?(info, &dimensionsCount), action: "GetDimensionsCount")
var dimensions = Array(repeating: Int64(0), count: dimensionsCount)
if dimensionsCount > 0 {
try check(
api.pointee.GetDimensions?(info, &dimensions, dimensionsCount),
action: "GetDimensions"
)
}
return ORTTensorTypeAndShapeInfo(
elementType: try ORTTensorElementDataType(native: elementType),
shape: dimensions.map(Int.init)
)
}
static func fetchValueTypeInfo(from value: OpaquePointer) throws -> ORTValueTypeInfo {
var typeInfo: OpaquePointer?
try check(api.pointee.GetTypeInfo?(value, &typeInfo), action: "GetTypeInfo")
guard let typeInfo else {
throw ORTError.runtimeFailure(action: "GetTypeInfo", message: "Type info was not returned")
}
defer {
api.pointee.ReleaseTypeInfo?(typeInfo)
}
var onnxType = ONNXType(rawValue: 0)
try check(api.pointee.GetOnnxTypeFromTypeInfo?(typeInfo, &onnxType), action: "GetOnnxTypeFromTypeInfo")
var tensorInfoPointer: OpaquePointer?
try check(api.pointee.CastTypeInfoToTensorInfo?(typeInfo, &tensorInfoPointer), action: "CastTypeInfoToTensorInfo")
let tensorInfo = try tensorInfoPointer.map { pointer in
try fetchTensorInfoView(from: pointer)
}
return ORTValueTypeInfo(
type: try ORTValueType(native: onnxType),
tensorTypeAndShapeInfo: tensorInfo
)
}
static func fetchTensorInfoView(from info: OpaquePointer) throws -> ORTTensorTypeAndShapeInfo {
var elementType = ONNXTensorElementDataType(rawValue: 0)
try check(api.pointee.GetTensorElementType?(info, &elementType), action: "GetTensorElementType")
var dimensionsCount: Int = 0
try check(api.pointee.GetDimensionsCount?(info, &dimensionsCount), action: "GetDimensionsCount")
var dimensions = Array(repeating: Int64(0), count: dimensionsCount)
if dimensionsCount > 0 {
try check(api.pointee.GetDimensions?(info, &dimensions, dimensionsCount), action: "GetDimensions")
}
return ORTTensorTypeAndShapeInfo(
elementType: try ORTTensorElementDataType(native: elementType),
shape: dimensions.map(Int.init)
)
}
static func boolString(_ value: Bool) -> String {
value ? "1" : "0"
}
}
@@ -0,0 +1,113 @@
import Foundation
import Testing
@preconcurrency import COnnxRuntime
@testable import OnnxRuntimeBindings
@_cdecl("SwiftTestRegisterCustomOps")
func SwiftTestRegisterCustomOps(_ options: OpaquePointer?, _ api: UnsafePointer<OrtApiBase>?) -> OpaquePointer? {
_ = options
_ = api
return nil
}
struct SwiftAPITests {
@Test
func sessionOptionsRetainConfiguredState() throws {
let options = try ORTSessionOptions()
try options.setIntraOpNumThreads(4)
try options.setGraphOptimizationLevel(.extended)
try options.setOptimizedModelFilePath("/tmp/model.optimized.onnx")
try options.setLogID("session-log")
try options.setLogSeverityLevel(.warning)
try options.addConfigEntry(key: "session.disable_prepacking", value: "1")
#expect(options.intraOpNumThreads == 4)
#expect(options.graphOptimizationLevel == .extended)
#expect(options.optimizedModelFilePath == "/tmp/model.optimized.onnx")
#expect(options.logID == "session-log")
#expect(options.logSeverityLevel == .warning)
#expect(options.configEntries["session.disable_prepacking"] == "1")
#expect(options.executionProviders.isEmpty)
}
@Test
func unsupportedExecutionProviderSurfacesRuntimeError() throws {
let options = try ORTSessionOptions()
#expect(throws: ORTError.self) {
try options.appendExecutionProvider("XNNPACK", providerOptions: ["threads": "2"])
}
}
@Test
func customOpsFunctionPointerRegistrationIsInvoked() throws {
let options = try ORTSessionOptions()
let functionPointer = unsafeBitCast(
SwiftTestRegisterCustomOps as RegisterCustomOpsFn,
to: UnsafeMutableRawPointer.self
)
try options.registerCustomOps(usingFunctionPointer: functionPointer)
#expect(options.customOpRegistrationFunctionPointer == functionPointer)
}
@Test
func runOptionsRetainConfiguredState() throws {
let options = try ORTRunOptions()
try options.setLogTag("inference")
try options.setLogSeverityLevel(.error)
try options.addConfigEntry(key: "terminate", value: "0")
#expect(options.logTag == "inference")
#expect(options.logSeverityLevel == .error)
#expect(options.configEntries["terminate"] == "0")
}
@Test
func numericTensorValueExposesTensorInfo() throws {
let bytes = NSMutableData(data: Data([0, 0, 128, 63]))
let value = try ORTValue(tensorData: bytes, elementType: .float, shape: [1])
let typeInfo = try value.typeInfo()
let tensorInfo = try value.tensorTypeAndShapeInfo()
let data = try value.tensorData()
#expect(typeInfo.type == .tensor)
#expect(typeInfo.tensorTypeAndShapeInfo?.elementType == .float)
#expect(tensorInfo.elementType == .float)
#expect(tensorInfo.shape == [1])
#expect(data.length == bytes.length)
#expect(Data(referencing: data) == Data(referencing: bytes))
}
@Test
func stringTensorValueExposesTensorInfo() throws {
let value = try ORTValue(tensorStringData: ["hello", "world"], shape: [2])
let typeInfo = try value.typeInfo()
let tensorInfo = try value.tensorTypeAndShapeInfo()
let strings = try value.tensorStringData()
#expect(typeInfo.type == .tensor)
#expect(typeInfo.tensorTypeAndShapeInfo?.elementType == .string)
#expect(tensorInfo.elementType == .string)
#expect(tensorInfo.shape == [2])
#expect(strings == ["hello", "world"])
}
@Test
func wrongTensorAccessorThrows() throws {
let numeric = try ORTValue(tensorData: NSMutableData(data: Data([0, 0, 0, 0])), elementType: .float, shape: [])
let string = try ORTValue(tensorStringData: [], shape: [0])
#expect(throws: ORTError.self) {
_ = try numeric.tensorStringData()
}
#expect(throws: ORTError.self) {
_ = try string.tensorData()
}
}
}