diff --git a/Package.swift b/Package.swift index 6d09521..167051f 100644 --- a/Package.swift +++ b/Package.swift @@ -31,7 +31,7 @@ let package = Package( .target(name: "OnnxRuntimeBindings", dependencies: ["onnxruntime"], path: "objectivec", - exclude: ["ReadMe.md", "format_objc.sh"], + exclude: ["test", "docs", "ReadMe.md", "format_objc.sh"], cxxSettings: [ .define("SPM_BUILD"), .unsafeFlags(["-std=c++17", diff --git a/objectivec/assert_arc_enabled.mm b/objectivec/assert_arc_enabled.mm new file mode 100644 index 0000000..9aa0bad --- /dev/null +++ b/objectivec/assert_arc_enabled.mm @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +static_assert(__has_feature(objc_arc), "Objective-C ARC must be enabled."); diff --git a/objectivec/docs/jazzy_config.yaml b/objectivec/docs/jazzy_config.yaml new file mode 100644 index 0000000..676f89d --- /dev/null +++ b/objectivec/docs/jazzy_config.yaml @@ -0,0 +1,13 @@ +module: ONNX Runtime Objective-C API +author: ONNX Runtime Authors +author_url: https://www.onnxruntime.ai +github_url: https://github.com/microsoft/onnxruntime + +objc: true +umbrella_header: ../include/onnxruntime.h +framework_root: .. + +readme: ./main_page.md + +hide_documentation_coverage: true +undocumented_text: "" diff --git a/objectivec/docs/main_page.md b/objectivec/docs/main_page.md new file mode 100644 index 0000000..fdc7c80 --- /dev/null +++ b/objectivec/docs/main_page.md @@ -0,0 +1,5 @@ +# ONNX Runtime Objective-C API + +ONNX Runtime provides an Objective-C API. + +It can be used from Objective-C/C++ or Swift with a bridging header. diff --git a/objectivec/docs/readme.md b/objectivec/docs/readme.md new file mode 100644 index 0000000..5697480 --- /dev/null +++ b/objectivec/docs/readme.md @@ -0,0 +1,17 @@ +# Objective-C API Documentation + +The API should be documented with comments in the [public header files](../include). + +## Documentation Generation + +The [Jazzy](https://github.com/realm/jazzy) tool is used to generate documentation from the code. + +To generate documentation, from the repo root, run: + +```bash +jazzy --config objectivec/docs/jazzy_config.yaml --output +``` + +The generated documentation website files will be in ``. + +[main_page.md](./main_page.md) contains content for the main page of the generated documentation website. diff --git a/objectivec/error_utils.mm b/objectivec/error_utils.mm new file mode 100644 index 0000000..9186326 --- /dev/null +++ b/objectivec/error_utils.mm @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "error_utils.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString* const kOrtErrorDomain = @"onnxruntime"; + +void ORTSaveCodeAndDescriptionToError(int code, const char* descriptionCstr, NSError** error) { + if (!error) return; + + NSString* description = [NSString stringWithCString:descriptionCstr + encoding:NSASCIIStringEncoding]; + + *error = [NSError errorWithDomain:kOrtErrorDomain + code:code + userInfo:@{NSLocalizedDescriptionKey : description}]; +} + +void ORTSaveOrtExceptionToError(const Ort::Exception& e, NSError** error) { + ORTSaveCodeAndDescriptionToError(e.GetOrtErrorCode(), e.what(), error); +} + +void ORTSaveExceptionToError(const std::exception& e, NSError** error) { + ORTSaveCodeAndDescriptionToError(ORT_RUNTIME_EXCEPTION, e.what(), error); +} + +NS_ASSUME_NONNULL_END diff --git a/objectivec/ort_coreml_execution_provider.mm b/objectivec/ort_coreml_execution_provider.mm new file mode 100644 index 0000000..6340fde --- /dev/null +++ b/objectivec/ort_coreml_execution_provider.mm @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "ort_coreml_execution_provider.h" + +#import "cxx_api.h" +#import "error_utils.h" +#import "ort_session_internal.h" + +NS_ASSUME_NONNULL_BEGIN + +BOOL ORTIsCoreMLExecutionProviderAvailable() { + return ORT_OBJC_API_COREML_EP_AVAILABLE ? YES : NO; +} + +@implementation ORTCoreMLExecutionProviderOptions + +@end + +@implementation ORTSessionOptions (ORTSessionOptionsCoreMLEP) + +- (BOOL)appendCoreMLExecutionProviderWithOptions:(ORTCoreMLExecutionProviderOptions*)options + error:(NSError**)error { +#if ORT_OBJC_API_COREML_EP_AVAILABLE + try { + const uint32_t flags = + (options.useCPUOnly ? COREML_FLAG_USE_CPU_ONLY : 0) | + (options.enableOnSubgraphs ? COREML_FLAG_ENABLE_ON_SUBGRAPH : 0) | + (options.onlyEnableForDevicesWithANE ? COREML_FLAG_ONLY_ENABLE_DEVICE_WITH_ANE : 0); + Ort::ThrowOnError(OrtSessionOptionsAppendExecutionProvider_CoreML( + [self CXXAPIOrtSessionOptions], flags)); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error); +#else // !ORT_OBJC_API_COREML_EP_AVAILABLE + static_cast(options); + ORTSaveCodeAndDescriptionToError(ORT_FAIL, "CoreML execution provider is not enabled.", error); + return NO; +#endif +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/ort_enums.mm b/objectivec/ort_enums.mm new file mode 100644 index 0000000..0144a33 --- /dev/null +++ b/objectivec/ort_enums.mm @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "ort_enums_internal.h" + +#include + +#import "cxx_api.h" + +namespace { + +struct LoggingLevelInfo { + ORTLoggingLevel logging_level; + OrtLoggingLevel capi_logging_level; +}; + +// supported ORT logging levels +// define the mapping from ORTLoggingLevel to C API OrtLoggingLevel here +constexpr LoggingLevelInfo kLoggingLevelInfos[]{ + {ORTLoggingLevelVerbose, ORT_LOGGING_LEVEL_VERBOSE}, + {ORTLoggingLevelInfo, ORT_LOGGING_LEVEL_INFO}, + {ORTLoggingLevelWarning, ORT_LOGGING_LEVEL_WARNING}, + {ORTLoggingLevelError, ORT_LOGGING_LEVEL_ERROR}, + {ORTLoggingLevelFatal, ORT_LOGGING_LEVEL_FATAL}, +}; + +struct ValueTypeInfo { + ORTValueType type; + ONNXType capi_type; +}; + +// supported ORT value types +// define the mapping from ORTValueType to C API ONNXType here +constexpr ValueTypeInfo kValueTypeInfos[]{ + {ORTValueTypeUnknown, ONNX_TYPE_UNKNOWN}, + {ORTValueTypeTensor, ONNX_TYPE_TENSOR}, +}; + +struct TensorElementTypeInfo { + ORTTensorElementDataType type; + ONNXTensorElementDataType capi_type; + size_t element_size; +}; + +// supported ORT tensor element data types +// define the mapping from ORTTensorElementDataType to C API ONNXTensorElementDataType here +constexpr TensorElementTypeInfo kElementTypeInfos[]{ + {ORTTensorElementDataTypeUndefined, ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED, 0}, + {ORTTensorElementDataTypeFloat, ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, sizeof(float)}, + {ORTTensorElementDataTypeInt8, ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8, sizeof(int8_t)}, + {ORTTensorElementDataTypeUInt8, ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8, sizeof(uint8_t)}, + {ORTTensorElementDataTypeInt32, ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32, sizeof(int32_t)}, + {ORTTensorElementDataTypeUInt32, ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT32, sizeof(uint32_t)}, + {ORTTensorElementDataTypeInt64, ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64, sizeof(int64_t)}, + {ORTTensorElementDataTypeUInt64, ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT64, sizeof(uint64_t)}, +}; + +struct GraphOptimizationLevelInfo { + ORTGraphOptimizationLevel opt_level; + GraphOptimizationLevel capi_opt_level; +}; + +// ORT graph optimization levels +// define the mapping from ORTGraphOptimizationLevel to C API GraphOptimizationLevel here +constexpr GraphOptimizationLevelInfo kGraphOptimizationLevelInfos[]{ + {ORTGraphOptimizationLevelNone, ORT_DISABLE_ALL}, + {ORTGraphOptimizationLevelBasic, ORT_ENABLE_BASIC}, + {ORTGraphOptimizationLevelExtended, ORT_ENABLE_EXTENDED}, + {ORTGraphOptimizationLevelAll, ORT_ENABLE_ALL}, +}; + +template +auto SelectAndTransform( + const Container& container, SelectFn select_fn, TransformFn transform_fn, + const char* not_found_msg) + -> decltype(transform_fn(*std::begin(container))) { + const auto it = std::find_if( + std::begin(container), std::end(container), select_fn); + if (it == std::end(container)) { + ORT_CXX_API_THROW(not_found_msg, ORT_NOT_IMPLEMENTED); + } + return transform_fn(*it); +} + +} // namespace + +OrtLoggingLevel PublicToCAPILoggingLevel(ORTLoggingLevel logging_level) { + return SelectAndTransform( + kLoggingLevelInfos, + [logging_level](const auto& logging_level_info) { return logging_level_info.logging_level == logging_level; }, + [](const auto& logging_level_info) { return logging_level_info.capi_logging_level; }, + "unsupported logging level"); +} + +ORTValueType CAPIToPublicValueType(ONNXType capi_type) { + return SelectAndTransform( + kValueTypeInfos, + [capi_type](const auto& type_info) { return type_info.capi_type == capi_type; }, + [](const auto& type_info) { return type_info.type; }, + "unsupported value type"); +} + +ONNXTensorElementDataType PublicToCAPITensorElementType(ORTTensorElementDataType type) { + return SelectAndTransform( + kElementTypeInfos, + [type](const auto& type_info) { return type_info.type == type; }, + [](const auto& type_info) { return type_info.capi_type; }, + "unsupported tensor element type"); +} + +ORTTensorElementDataType CAPIToPublicTensorElementType(ONNXTensorElementDataType capi_type) { + return SelectAndTransform( + kElementTypeInfos, + [capi_type](const auto& type_info) { return type_info.capi_type == capi_type; }, + [](const auto& type_info) { return type_info.type; }, + "unsupported tensor element type"); +} + +size_t SizeOfCAPITensorElementType(ONNXTensorElementDataType capi_type) { + return SelectAndTransform( + kElementTypeInfos, + [capi_type](const auto& type_info) { return type_info.capi_type == capi_type; }, + [](const auto& type_info) { return type_info.element_size; }, + "unsupported tensor element type"); +} + +GraphOptimizationLevel PublicToCAPIGraphOptimizationLevel(ORTGraphOptimizationLevel opt_level) { + return SelectAndTransform( + kGraphOptimizationLevelInfos, + [opt_level](const auto& opt_level_info) { return opt_level_info.opt_level == opt_level; }, + [](const auto& opt_level_info) { return opt_level_info.capi_opt_level; }, + "unsupported graph optimization level"); +} diff --git a/objectivec/ort_env.mm b/objectivec/ort_env.mm new file mode 100644 index 0000000..cd83ac5 --- /dev/null +++ b/objectivec/ort_env.mm @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "ort_env_internal.h" + +#include + +#import "cxx_api.h" + +#import "error_utils.h" +#import "ort_enums_internal.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString* ORTVersion(void) { + std::string result = OrtGetApiBase()->GetVersionString(); + return [NSString stringWithUTF8String:result.c_str()]; +} + +@implementation ORTEnv { + std::optional _env; +} + +- (nullable instancetype)initWithLoggingLevel:(ORTLoggingLevel)loggingLevel + error:(NSError**)error { + if ((self = [super init]) == nil) { + return nil; + } + + try { + const auto CAPILoggingLevel = PublicToCAPILoggingLevel(loggingLevel); + _env = Ort::Env{CAPILoggingLevel}; + return self; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error) +} + +- (Ort::Env&)CXXAPIOrtEnv { + return *_env; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/ort_session.mm b/objectivec/ort_session.mm new file mode 100644 index 0000000..c4f57b0 --- /dev/null +++ b/objectivec/ort_session.mm @@ -0,0 +1,361 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "ort_session_internal.h" + +#include +#include + +#import "cxx_api.h" +#import "error_utils.h" +#import "ort_enums_internal.h" +#import "ort_env_internal.h" +#import "ort_value_internal.h" + +namespace { +enum class NamedValueType { + Input, + OverridableInitializer, + Output, +}; +} // namespace + +NS_ASSUME_NONNULL_BEGIN + +@implementation ORTSession { + std::optional _session; +} + +#pragma mark - Public + +- (nullable instancetype)initWithEnv:(ORTEnv*)env + modelPath:(NSString*)path + sessionOptions:(nullable ORTSessionOptions*)sessionOptions + error:(NSError**)error { + if ((self = [super init]) == nil) { + return nil; + } + + try { + if (!sessionOptions) { + sessionOptions = [[ORTSessionOptions alloc] initWithError:error]; + if (!sessionOptions) { + return nil; + } + } + + _session = Ort::Session{[env CXXAPIOrtEnv], + path.UTF8String, + [sessionOptions CXXAPIOrtSessionOptions]}; + + return self; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error) +} + +- (BOOL)runWithInputs:(NSDictionary*)inputs + outputs:(NSDictionary*)outputs + runOptions:(nullable ORTRunOptions*)runOptions + error:(NSError**)error { + try { + if (!runOptions) { + runOptions = [[ORTRunOptions alloc] initWithError:error]; + if (!runOptions) { + return NO; + } + } + + std::vector inputNames, outputNames; + std::vector inputValues; + std::vector outputValues; + + for (NSString* inputName in inputs) { + inputNames.push_back(inputName.UTF8String); + inputValues.push_back(static_cast([inputs[inputName] CXXAPIOrtValue])); + } + + for (NSString* outputName in outputs) { + outputNames.push_back(outputName.UTF8String); + outputValues.push_back(static_cast([outputs[outputName] CXXAPIOrtValue])); + } + + Ort::ThrowOnError(Ort::GetApi().Run(*_session, [runOptions CXXAPIOrtRunOptions], + inputNames.data(), inputValues.data(), inputNames.size(), + outputNames.data(), outputNames.size(), outputValues.data())); + + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error) +} + +- (nullable NSDictionary*)runWithInputs:(NSDictionary*)inputs + outputNames:(NSSet*)outputNameSet + runOptions:(nullable ORTRunOptions*)runOptions + error:(NSError**)error { + try { + if (!runOptions) { + runOptions = [[ORTRunOptions alloc] initWithError:error]; + if (!runOptions) { + return nil; + } + } + + NSArray* outputNameArray = outputNameSet.allObjects; + + std::vector inputNames, outputNames; + std::vector inputValues; + std::vector outputValues; + + for (NSString* inputName in inputs) { + inputNames.push_back(inputName.UTF8String); + inputValues.push_back(static_cast([inputs[inputName] CXXAPIOrtValue])); + } + + for (NSString* outputName in outputNameArray) { + outputNames.push_back(outputName.UTF8String); + outputValues.push_back(nullptr); + } + + Ort::ThrowOnError(Ort::GetApi().Run(*_session, [runOptions CXXAPIOrtRunOptions], + inputNames.data(), inputValues.data(), inputNames.size(), + outputNames.data(), outputNames.size(), outputValues.data())); + + NSMutableDictionary* outputs = [[NSMutableDictionary alloc] init]; + for (NSUInteger i = 0; i < outputNameArray.count; ++i) { + ORTValue* outputValue = [[ORTValue alloc] initWithCAPIOrtValue:outputValues[i] externalTensorData:nil error:error]; + if (!outputValue) { + // clean up remaining C API OrtValues which haven't been wrapped by an ORTValue yet + for (NSUInteger j = i; j < outputNameArray.count; ++j) { + Ort::GetApi().ReleaseValue(outputValues[j]); + } + return nil; + } + + outputs[outputNameArray[i]] = outputValue; + } + + return outputs; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error) +} + +- (nullable NSArray*)inputNamesWithError:(NSError**)error { + return [self namesWithType:NamedValueType::Input error:error]; +} + +- (nullable NSArray*)overridableInitializerNamesWithError:(NSError**)error { + return [self namesWithType:NamedValueType::OverridableInitializer error:error]; +} + +- (nullable NSArray*)outputNamesWithError:(NSError**)error { + return [self namesWithType:NamedValueType::Output error:error]; +} + +#pragma mark - Private + +- (nullable NSArray*)namesWithType:(NamedValueType)namedValueType + error:(NSError**)error { + try { + auto getCount = [&session = *_session, namedValueType]() { + if (namedValueType == NamedValueType::Input) { + return session.GetInputCount(); + } else if (namedValueType == NamedValueType::OverridableInitializer) { + return session.GetOverridableInitializerCount(); + } else { + return session.GetOutputCount(); + } + }; + + auto getName = [&session = *_session, namedValueType](size_t i, OrtAllocator* allocator) { + if (namedValueType == NamedValueType::Input) { + return session.GetInputNameAllocated(i, allocator); + } else if (namedValueType == NamedValueType::OverridableInitializer) { + return session.GetOverridableInitializerNameAllocated(i, allocator); + } else { + return session.GetOutputNameAllocated(i, allocator); + } + }; + + const size_t nameCount = getCount(); + + Ort::AllocatorWithDefaultOptions allocator; + NSMutableArray* result = [NSMutableArray arrayWithCapacity:nameCount]; + + for (size_t i = 0; i < nameCount; ++i) { + auto name = getName(i, allocator); + NSString* nameNsstr = [NSString stringWithUTF8String:name.get()]; + NSAssert(nameNsstr != nil, @"nameNsstr must not be nil"); + [result addObject:nameNsstr]; + } + + return result; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error) +} + +@end + +@implementation ORTSessionOptions { + std::optional _sessionOptions; +} + +#pragma mark - Public + +- (nullable instancetype)initWithError:(NSError**)error { + if ((self = [super init]) == nil) { + return nil; + } + + try { + _sessionOptions = Ort::SessionOptions{}; + return self; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error) +} + +- (BOOL)appendExecutionProvider:(NSString*)providerName + providerOptions:(NSDictionary*)providerOptions + error:(NSError**)error { + try { + std::unordered_map options; + NSArray* keys = [providerOptions allKeys]; + + for (NSString* key in keys) { + NSString* value = [providerOptions objectForKey:key]; + options.emplace(key.UTF8String, value.UTF8String); + } + + _sessionOptions->AppendExecutionProvider(providerName.UTF8String, options); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error); +} + +- (BOOL)setIntraOpNumThreads:(int)intraOpNumThreads + error:(NSError**)error { + try { + _sessionOptions->SetIntraOpNumThreads(intraOpNumThreads); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error) +} + +- (BOOL)setGraphOptimizationLevel:(ORTGraphOptimizationLevel)graphOptimizationLevel + error:(NSError**)error { + try { + _sessionOptions->SetGraphOptimizationLevel( + PublicToCAPIGraphOptimizationLevel(graphOptimizationLevel)); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error) +} + +- (BOOL)setOptimizedModelFilePath:(NSString*)optimizedModelFilePath + error:(NSError**)error { + try { + _sessionOptions->SetOptimizedModelFilePath(optimizedModelFilePath.UTF8String); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error) +} + +- (BOOL)setLogID:(NSString*)logID + error:(NSError**)error { + try { + _sessionOptions->SetLogId(logID.UTF8String); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error) +} + +- (BOOL)setLogSeverityLevel:(ORTLoggingLevel)loggingLevel + error:(NSError**)error { + try { + _sessionOptions->SetLogSeverityLevel(PublicToCAPILoggingLevel(loggingLevel)); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error) +} + +- (BOOL)addConfigEntryWithKey:(NSString*)key + value:(NSString*)value + error:(NSError**)error { + try { + _sessionOptions->AddConfigEntry(key.UTF8String, value.UTF8String); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error) +} + +- (BOOL)registerCustomOpsUsingFunction:(NSString*)registrationFuncName + error:(NSError**)error { + try { + _sessionOptions->RegisterCustomOpsUsingFunction(registrationFuncName.UTF8String); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error) +} + +#pragma mark - Internal + +- (Ort::SessionOptions&)CXXAPIOrtSessionOptions { + return *_sessionOptions; +} + +@end + +@implementation ORTRunOptions { + std::optional _runOptions; +} + +#pragma mark - Public + +- (nullable instancetype)initWithError:(NSError**)error { + if ((self = [super init]) == nil) { + return nil; + } + + try { + _runOptions = Ort::RunOptions{}; + return self; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error) +} + +- (BOOL)setLogTag:(NSString*)logTag + error:(NSError**)error { + try { + _runOptions->SetRunTag(logTag.UTF8String); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error) +} + +- (BOOL)setLogSeverityLevel:(ORTLoggingLevel)loggingLevel + error:(NSError**)error { + try { + _runOptions->SetRunLogSeverityLevel(PublicToCAPILoggingLevel(loggingLevel)); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error) +} + +- (BOOL)addConfigEntryWithKey:(NSString*)key + value:(NSString*)value + error:(NSError**)error { + try { + _runOptions->AddConfigEntry(key.UTF8String, value.UTF8String); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error) +} + +#pragma mark - Internal + +- (Ort::RunOptions&)CXXAPIOrtRunOptions { + return *_runOptions; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/ort_value.mm b/objectivec/ort_value.mm new file mode 100644 index 0000000..ce77ca7 --- /dev/null +++ b/objectivec/ort_value.mm @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "ort_value_internal.h" + +#include + +#import "cxx_api.h" +#import "error_utils.h" +#import "ort_enums_internal.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace { + +ORTTensorTypeAndShapeInfo* CXXAPIToPublicTensorTypeAndShapeInfo( + const Ort::ConstTensorTypeAndShapeInfo& CXXAPITensorTypeAndShapeInfo) { + auto* result = [[ORTTensorTypeAndShapeInfo alloc] init]; + const auto elementType = CXXAPITensorTypeAndShapeInfo.GetElementType(); + const std::vector shape = CXXAPITensorTypeAndShapeInfo.GetShape(); + + result.elementType = CAPIToPublicTensorElementType(elementType); + auto* shapeArray = [[NSMutableArray alloc] initWithCapacity:shape.size()]; + for (size_t i = 0; i < shape.size(); ++i) { + shapeArray[i] = @(shape[i]); + } + result.shape = shapeArray; + + return result; +} + +ORTValueTypeInfo* CXXAPIToPublicValueTypeInfo( + const Ort::TypeInfo& CXXAPITypeInfo) { + auto* result = [[ORTValueTypeInfo alloc] init]; + const auto valueType = CXXAPITypeInfo.GetONNXType(); + + result.type = CAPIToPublicValueType(valueType); + + if (valueType == ONNX_TYPE_TENSOR) { + const auto tensorTypeAndShapeInfo = CXXAPITypeInfo.GetTensorTypeAndShapeInfo(); + result.tensorTypeAndShapeInfo = CXXAPIToPublicTensorTypeAndShapeInfo(tensorTypeAndShapeInfo); + } + + return result; +} + +// out = a * b +// returns true iff the result does not overflow +bool SafeMultiply(size_t a, size_t b, size_t& out) { + return !__builtin_mul_overflow(a, b, &out); +} + +} // namespace + +@interface ORTValue () + +// pointer to any external tensor data to keep alive for the lifetime of the ORTValue +@property(nonatomic, nullable) NSMutableData* externalTensorData; + +@end + +@implementation ORTValue { + std::optional _value; + std::optional _typeInfo; +} + +#pragma mark - Public + +- (nullable instancetype)initWithTensorData:(NSMutableData*)tensorData + elementType:(ORTTensorElementDataType)elementType + shape:(NSArray*)shape + error:(NSError**)error { + try { + const auto memoryInfo = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU); + const auto ONNXElementType = PublicToCAPITensorElementType(elementType); + const auto shapeVector = [shape]() { + std::vector result{}; + result.reserve(shape.count); + for (NSNumber* dim in shape) { + result.push_back(dim.longLongValue); + } + return result; + }(); + Ort::Value ortValue = Ort::Value::CreateTensor( + memoryInfo, tensorData.mutableBytes, tensorData.length, + shapeVector.data(), shapeVector.size(), ONNXElementType); + + return [self initWithCAPIOrtValue:ortValue.release() + externalTensorData:tensorData + error:error]; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error) +} + +- (nullable ORTValueTypeInfo*)typeInfoWithError:(NSError**)error { + try { + return CXXAPIToPublicValueTypeInfo(*_typeInfo); + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error) +} + +- (nullable ORTTensorTypeAndShapeInfo*)tensorTypeAndShapeInfoWithError:(NSError**)error { + try { + const auto tensorTypeAndShapeInfo = _typeInfo->GetTensorTypeAndShapeInfo(); + return CXXAPIToPublicTensorTypeAndShapeInfo(tensorTypeAndShapeInfo); + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error) +} + +- (nullable NSMutableData*)tensorDataWithError:(NSError**)error { + try { + const auto tensorTypeAndShapeInfo = _typeInfo->GetTensorTypeAndShapeInfo(); + const size_t elementCount = tensorTypeAndShapeInfo.GetElementCount(); + const size_t elementSize = SizeOfCAPITensorElementType(tensorTypeAndShapeInfo.GetElementType()); + size_t rawDataLength; + if (!SafeMultiply(elementCount, elementSize, rawDataLength)) { + ORT_CXX_API_THROW("failed to compute tensor data length", ORT_RUNTIME_EXCEPTION); + } + + void* rawData; + Ort::ThrowOnError(Ort::GetApi().GetTensorMutableData(*_value, &rawData)); + + return [NSMutableData dataWithBytesNoCopy:rawData + length:rawDataLength + freeWhenDone:NO]; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error) +} + +#pragma mark - Internal + +- (nullable instancetype)initWithCAPIOrtValue:(OrtValue*)CAPIOrtValue + externalTensorData:(nullable NSMutableData*)externalTensorData + error:(NSError**)error { + if ((self = [super init]) == nil) { + return nil; + } + + try { + _value = Ort::Value{CAPIOrtValue}; + _typeInfo = _value->GetTypeInfo(); + _externalTensorData = externalTensorData; + return self; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error); +} + +- (Ort::Value&)CXXAPIOrtValue { + return *_value; +} + +@end + +@implementation ORTValueTypeInfo +@end + +@implementation ORTTensorTypeAndShapeInfo +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/ort_xnnpack_execution_provider.mm b/objectivec/ort_xnnpack_execution_provider.mm new file mode 100644 index 0000000..324974a --- /dev/null +++ b/objectivec/ort_xnnpack_execution_provider.mm @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "ort_xnnpack_execution_provider.h" + +#import "cxx_api.h" +#import "error_utils.h" +#import "ort_session_internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation ORTXnnpackExecutionProviderOptions + +@end + +@implementation ORTSessionOptions (ORTSessionOptionsXnnpackEP) + +- (BOOL)appendXnnpackExecutionProviderWithOptions:(ORTXnnpackExecutionProviderOptions*)options + error:(NSError**)error { + try { + NSDictionary* provider_options = @{ + @"intra_op_num_threads" : [NSString stringWithFormat:@"%d", options.intra_op_num_threads] + }; + return [self appendExecutionProvider:@"XNNPACK" providerOptions:provider_options error:error]; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/assert_arc_enabled.mm b/objectivec/test/assert_arc_enabled.mm new file mode 100644 index 0000000..9aa0bad --- /dev/null +++ b/objectivec/test/assert_arc_enabled.mm @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +static_assert(__has_feature(objc_arc), "Objective-C ARC must be enabled."); diff --git a/objectivec/test/assertion_utils.h b/objectivec/test/assertion_utils.h new file mode 100644 index 0000000..f2b73e6 --- /dev/null +++ b/objectivec/test/assertion_utils.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +#define ORTAssertNullableResultSuccessful(result, error) \ + do { \ + XCTAssertNotNil(result, @"Expected non-nil result but got nil. Error: %@", error); \ + XCTAssertNil(error); \ + } while (0) + +#define ORTAssertBoolResultSuccessful(result, error) \ + do { \ + XCTAssertTrue(result, @"Expected true result but got false. Error: %@", error); \ + XCTAssertNil(error); \ + } while (0) + +#define ORTAssertNullableResultUnsuccessful(result, error) \ + do { \ + XCTAssertNil(result); \ + XCTAssertNotNil(error); \ + } while (0) + +#define ORTAssertBoolResultUnsuccessful(result, error) \ + do { \ + XCTAssertFalse(result); \ + XCTAssertNotNil(error); \ + } while (0) + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/ort_env_test.mm b/objectivec/test/ort_env_test.mm new file mode 100644 index 0000000..420f55f --- /dev/null +++ b/objectivec/test/ort_env_test.mm @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "ort_env.h" + +#import "test/assertion_utils.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTEnvTest : XCTestCase +@end + +@implementation ORTEnvTest +- (void)testGetOrtVersion { + NSString* ver = ORTVersion(); + XCTAssertNotNil(ver); +} + +- (void)testInitOk { + NSError* err = nil; + ORTEnv* env = [[ORTEnv alloc] initWithLoggingLevel:ORTLoggingLevelWarning + error:&err]; + ORTAssertNullableResultSuccessful(env, err); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/ort_session_test.mm b/objectivec/test/ort_session_test.mm new file mode 100644 index 0000000..790ad1c --- /dev/null +++ b/objectivec/test/ort_session_test.mm @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "ort_coreml_execution_provider.h" +#import "ort_xnnpack_execution_provider.h" +#import "ort_env.h" +#import "ort_session.h" +#import "ort_value.h" + +#import "test/assertion_utils.h" + +#include + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTSessionTest : XCTestCase + +@property(readonly, nullable) ORTEnv* ortEnv; + +@end + +@implementation ORTSessionTest + +- (void)setUp { + [super setUp]; + + self.continueAfterFailure = NO; + + NSError* err = nil; + _ortEnv = [[ORTEnv alloc] initWithLoggingLevel:ORTLoggingLevelWarning + error:&err]; + ORTAssertNullableResultSuccessful(_ortEnv, err); +} + +- (void)tearDown { + _ortEnv = nil; + + [super tearDown]; +} + +// model with an Add op +// inputs: A, B +// output: C = A + B ++ (NSString*)getAddModelPath { + NSBundle* bundle = [NSBundle bundleForClass:[ORTSessionTest class]]; + NSString* path = [bundle pathForResource:@"single_add.basic" + ofType:@"ort"]; + return path; +} + ++ (NSMutableData*)dataWithScalarFloat:(float)value { + NSMutableData* data = [[NSMutableData alloc] initWithBytes:&value length:sizeof(value)]; + return data; +} + ++ (ORTValue*)ortValueWithScalarFloatData:(NSMutableData*)data { + NSArray* shape = @[ @1 ]; + NSError* err = nil; + ORTValue* ortValue = [[ORTValue alloc] initWithTensorData:data + elementType:ORTTensorElementDataTypeFloat + shape:shape + error:&err]; + ORTAssertNullableResultSuccessful(ortValue, err); + return ortValue; +} + ++ (ORTSessionOptions*)makeSessionOptions { + NSError* err = nil; + ORTSessionOptions* sessionOptions = [[ORTSessionOptions alloc] initWithError:&err]; + ORTAssertNullableResultSuccessful(sessionOptions, err); + return sessionOptions; +} + ++ (ORTRunOptions*)makeRunOptions { + NSError* err = nil; + ORTRunOptions* runOptions = [[ORTRunOptions alloc] initWithError:&err]; + ORTAssertNullableResultSuccessful(runOptions, err); + return runOptions; +} + +- (void)testInitAndRunWithPreallocatedOutputOk { + NSMutableData* aData = [ORTSessionTest dataWithScalarFloat:1.0f]; + NSMutableData* bData = [ORTSessionTest dataWithScalarFloat:2.0f]; + NSMutableData* cData = [ORTSessionTest dataWithScalarFloat:0.0f]; + + ORTValue* a = [ORTSessionTest ortValueWithScalarFloatData:aData]; + ORTValue* b = [ORTSessionTest ortValueWithScalarFloatData:bData]; + ORTValue* c = [ORTSessionTest ortValueWithScalarFloatData:cData]; + + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultSuccessful(session, err); + + BOOL runResult = [session runWithInputs:@{@"A" : a, @"B" : b} + outputs:@{@"C" : c} + runOptions:[ORTSessionTest makeRunOptions] + error:&err]; + ORTAssertBoolResultSuccessful(runResult, err); + + const float cExpected = 3.0f; + float cActual; + memcpy(&cActual, cData.bytes, sizeof(float)); + XCTAssertEqual(cActual, cExpected); +} + +- (void)testInitAndRunOk { + NSMutableData* aData = [ORTSessionTest dataWithScalarFloat:1.0f]; + NSMutableData* bData = [ORTSessionTest dataWithScalarFloat:2.0f]; + + ORTValue* a = [ORTSessionTest ortValueWithScalarFloatData:aData]; + ORTValue* b = [ORTSessionTest ortValueWithScalarFloatData:bData]; + + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultSuccessful(session, err); + + NSDictionary* outputs = + [session runWithInputs:@{@"A" : a, @"B" : b} + outputNames:[NSSet setWithArray:@[ @"C" ]] + runOptions:[ORTSessionTest makeRunOptions] + error:&err]; + ORTAssertNullableResultSuccessful(outputs, err); + + ORTValue* cOutput = outputs[@"C"]; + XCTAssertNotNil(cOutput); + + NSData* cData = [cOutput tensorDataWithError:&err]; + ORTAssertNullableResultSuccessful(cData, err); + + const float cExpected = 3.0f; + float cActual; + memcpy(&cActual, cData.bytes, sizeof(float)); + XCTAssertEqual(cActual, cExpected); +} + +- (void)testGetNamesOk { + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultSuccessful(session, err); + + NSArray* inputNames = [session inputNamesWithError:&err]; + ORTAssertNullableResultSuccessful(inputNames, err); + XCTAssertEqualObjects(inputNames, (@[ @"A", @"B" ])); + + NSArray* overridableInitializerNames = [session overridableInitializerNamesWithError:&err]; + ORTAssertNullableResultSuccessful(overridableInitializerNames, err); + XCTAssertEqualObjects(overridableInitializerNames, (@[])); + + NSArray* outputNames = [session outputNamesWithError:&err]; + ORTAssertNullableResultSuccessful(outputNames, err); + XCTAssertEqualObjects(outputNames, (@[ @"C" ])); +} + +- (void)testInitFailsWithInvalidPath { + NSString* invalidModelPath = @"invalid/path/to/model.ort"; + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:invalidModelPath + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultUnsuccessful(session, err); +} + +- (void)testRunFailsWithInvalidInput { + NSMutableData* dData = [ORTSessionTest dataWithScalarFloat:1.0f]; + NSMutableData* cData = [ORTSessionTest dataWithScalarFloat:0.0f]; + + ORTValue* d = [ORTSessionTest ortValueWithScalarFloatData:dData]; + ORTValue* c = [ORTSessionTest ortValueWithScalarFloatData:cData]; + + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultSuccessful(session, err); + + BOOL runResult = [session runWithInputs:@{@"D" : d} + outputs:@{@"C" : c} + runOptions:[ORTSessionTest makeRunOptions] + error:&err]; + ORTAssertBoolResultUnsuccessful(runResult, err); +} + +- (void)testAppendCoreMLEP { + NSError* err = nil; + ORTSessionOptions* sessionOptions = [ORTSessionTest makeSessionOptions]; + ORTCoreMLExecutionProviderOptions* coreMLOptions = [[ORTCoreMLExecutionProviderOptions alloc] init]; + coreMLOptions.enableOnSubgraphs = YES; // set an arbitrary option + + BOOL appendResult = [sessionOptions appendCoreMLExecutionProviderWithOptions:coreMLOptions + error:&err]; + + if (!ORTIsCoreMLExecutionProviderAvailable()) { + ORTAssertBoolResultUnsuccessful(appendResult, err); + return; + } + + ORTAssertBoolResultSuccessful(appendResult, err); + + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:sessionOptions + error:&err]; + ORTAssertNullableResultSuccessful(session, err); +} + +- (void)testAppendXnnpackEP { + NSError* err = nil; + ORTSessionOptions* sessionOptions = [ORTSessionTest makeSessionOptions]; + ORTXnnpackExecutionProviderOptions* XnnpackOptions = [[ORTXnnpackExecutionProviderOptions alloc] init]; + XnnpackOptions.intra_op_num_threads = 2; + + BOOL appendResult = [sessionOptions appendXnnpackExecutionProviderWithOptions:XnnpackOptions + error:&err]; + // Without xnnpack EP in building also can pass the test + NSString* err_msg = [err localizedDescription]; + if (!appendResult && [err_msg containsString:@"XNNPACK execution provider is not supported in this build. "]) { + return; + } + + ORTAssertBoolResultSuccessful(appendResult, err); + + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:sessionOptions + error:&err]; + ORTAssertNullableResultSuccessful(session, err); +} +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/ort_value_test.mm b/objectivec/test/ort_value_test.mm new file mode 100644 index 0000000..734ad39 --- /dev/null +++ b/objectivec/test/ort_value_test.mm @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "ort_value.h" + +#include + +#import "test/assertion_utils.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTValueTest : XCTestCase +@end + +@implementation ORTValueTest + +- (void)setUp { + [super setUp]; + + self.continueAfterFailure = NO; +} + +- (void)testInitTensorOk { + int32_t value = 42; + NSMutableData* data = [[NSMutableData alloc] initWithBytes:&value + length:sizeof(int32_t)]; + NSArray* shape = @[ @1 ]; + + const ORTTensorElementDataType elementType = ORTTensorElementDataTypeInt32; + + NSError* err = nil; + ORTValue* ortValue = [[ORTValue alloc] initWithTensorData:data + elementType:elementType + shape:shape + error:&err]; + ORTAssertNullableResultSuccessful(ortValue, err); + + auto checkTensorInfo = [&](ORTTensorTypeAndShapeInfo* tensorInfo) { + XCTAssertEqual(tensorInfo.elementType, elementType); + XCTAssertEqualObjects(tensorInfo.shape, shape); + }; + + ORTValueTypeInfo* typeInfo = [ortValue typeInfoWithError:&err]; + ORTAssertNullableResultSuccessful(typeInfo, err); + XCTAssertEqual(typeInfo.type, ORTValueTypeTensor); + XCTAssertNotNil(typeInfo.tensorTypeAndShapeInfo); + checkTensorInfo(typeInfo.tensorTypeAndShapeInfo); + + ORTTensorTypeAndShapeInfo* tensorInfo = [ortValue tensorTypeAndShapeInfoWithError:&err]; + ORTAssertNullableResultSuccessful(tensorInfo, err); + checkTensorInfo(tensorInfo); + + NSData* actualData = [ortValue tensorDataWithError:&err]; + ORTAssertNullableResultSuccessful(actualData, err); + XCTAssertEqual(actualData.length, sizeof(int32_t)); + int32_t actualValue; + memcpy(&actualValue, actualData.bytes, sizeof(int32_t)); + XCTAssertEqual(actualValue, value); +} + +- (void)testInitTensorFailsWithDataSmallerThanShape { + std::vector values{1, 2, 3, 4}; + NSMutableData* data = [[NSMutableData alloc] initWithBytes:values.data() + length:values.size() * sizeof(int32_t)]; + NSArray* shape = @[ @2, @3 ]; // too large + + NSError* err = nil; + ORTValue* ortValue = [[ORTValue alloc] initWithTensorData:data + elementType:ORTTensorElementDataTypeInt32 + shape:shape + error:&err]; + ORTAssertNullableResultUnsuccessful(ortValue, err); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/testdata/gen_models.sh b/objectivec/test/testdata/gen_models.sh new file mode 100755 index 0000000..0a58ca8 --- /dev/null +++ b/objectivec/test/testdata/gen_models.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +# Get directory this script is in +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +cd ${DIR} + +python3 ./single_add_gen.py + +ORT_CONVERT_ONNX_MODELS_TO_ORT_OPTIMIZATION_LEVEL=basic python3 -m onnxruntime.tools.convert_onnx_models_to_ort --optimization_style=Fixed . diff --git a/objectivec/test/testdata/single_add.basic.ort b/objectivec/test/testdata/single_add.basic.ort new file mode 100644 index 0000000..f622784 Binary files /dev/null and b/objectivec/test/testdata/single_add.basic.ort differ diff --git a/objectivec/test/testdata/single_add.onnx b/objectivec/test/testdata/single_add.onnx new file mode 100644 index 0000000..82e8fe6 Binary files /dev/null and b/objectivec/test/testdata/single_add.onnx differ diff --git a/objectivec/test/testdata/single_add_gen.py b/objectivec/test/testdata/single_add_gen.py new file mode 100644 index 0000000..15b4372 --- /dev/null +++ b/objectivec/test/testdata/single_add_gen.py @@ -0,0 +1,19 @@ +import onnx +from onnx import TensorProto, helper + +graph = helper.make_graph( + [ # nodes + helper.make_node("Add", ["A", "B"], ["C"], "Add"), + ], + "SingleAdd", # name + [ # inputs + helper.make_tensor_value_info("A", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info("B", TensorProto.FLOAT, [1]), + ], + [ # outputs + helper.make_tensor_value_info("C", TensorProto.FLOAT, [1]), + ], +) + +model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 12)]) +onnx.save(model, r"single_add.onnx")