From d21304ceb09d1b91160390ec80379b04c2bddedb Mon Sep 17 00:00:00 2001 From: Edward Chen <18449977+edgchen1@users.noreply.github.com> Date: Tue, 27 Apr 2021 10:06:30 -0700 Subject: [PATCH] Initial Objective-C API (#7366) Initial implementation of an Objective-C API. --- cmake/CMakeLists.txt | 6 + cmake/onnxruntime.cmake | 1 - cmake/onnxruntime_common.cmake | 3 +- cmake/onnxruntime_objectivec.cmake | 124 ++++++++++ cmake/onnxruntime_providers.cmake | 2 +- docs/Coding_Conventions_and_Standards.md | 5 + .../core/session/onnxruntime_c_api.h | 36 +-- objectivec/common/assert_arc_enabled.mm | 4 + objectivec/format_objc.sh | 10 + objectivec/include/onnxruntime.h | 9 + objectivec/include/onnxruntime/ort_env.h | 25 ++ objectivec/include/onnxruntime/ort_session.h | 59 +++++ objectivec/include/onnxruntime/ort_value.h | 88 +++++++ objectivec/src/error_utils.h | 13 ++ objectivec/src/error_utils.mm | 25 ++ objectivec/src/ort_env.mm | 37 +++ objectivec/src/ort_env_internal.h | 16 ++ objectivec/src/ort_session.mm | 118 ++++++++++ objectivec/src/ort_value.mm | 215 ++++++++++++++++++ objectivec/src/ort_value_internal.h | 20 ++ objectivec/test/ort_env_test.mm | 24 ++ objectivec/test/ort_session_test.mm | 162 +++++++++++++ objectivec/test/ort_value_test.mm | 78 +++++++ objectivec/test/testdata/single_add.onnx | 16 ++ objectivec/test/testdata/single_add_gen.py | 19 ++ .../core/framework/tensor_type_and_shape.cc | 24 +- .../core/framework/tensor_type_and_shape.h | 6 + tools/ci_build/build.py | 6 + .../azure-pipelines/mac-ci-pipeline.yml | 13 +- 29 files changed, 1131 insertions(+), 33 deletions(-) create mode 100644 cmake/onnxruntime_objectivec.cmake create mode 100644 objectivec/common/assert_arc_enabled.mm create mode 100755 objectivec/format_objc.sh create mode 100644 objectivec/include/onnxruntime.h create mode 100644 objectivec/include/onnxruntime/ort_env.h create mode 100644 objectivec/include/onnxruntime/ort_session.h create mode 100644 objectivec/include/onnxruntime/ort_value.h create mode 100644 objectivec/src/error_utils.h create mode 100644 objectivec/src/error_utils.mm create mode 100644 objectivec/src/ort_env.mm create mode 100644 objectivec/src/ort_env_internal.h create mode 100644 objectivec/src/ort_session.mm create mode 100644 objectivec/src/ort_value.mm create mode 100644 objectivec/src/ort_value_internal.h create mode 100644 objectivec/test/ort_env_test.mm create mode 100644 objectivec/test/ort_session_test.mm create mode 100644 objectivec/test/ort_value_test.mm create mode 100644 objectivec/test/testdata/single_add.onnx create mode 100644 objectivec/test/testdata/single_add_gen.py diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 65700a09b1..ac9c62fb6d 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -65,6 +65,7 @@ option(onnxruntime_MSVC_STATIC_RUNTIME "Compile for the static CRT" OFF) option(onnxruntime_GCC_STATIC_CPP_RUNTIME "Compile for the static libstdc++" OFF) option(onnxruntime_BUILD_UNIT_TESTS "Build ONNXRuntime unit tests" ON) option(onnxruntime_BUILD_CSHARP "Build C# library" OFF) +option(onnxruntime_BUILD_OBJC "Build Objective-C library" OFF) option(onnxruntime_USE_PREINSTALLED_EIGEN "Use pre-installed EIGEN. Need to provide eigen_SOURCE_PATH if turn this on." OFF) option(onnxruntime_BUILD_BENCHMARKS "Build ONNXRuntime micro-benchmarks" OFF) @@ -1574,6 +1575,11 @@ if (onnxruntime_ENABLE_PYTHON) include(onnxruntime_python.cmake) endif() +if (onnxruntime_BUILD_OBJC) + message(STATUS "Objective-C Build is enabled") + include(onnxruntime_objectivec.cmake) +endif() + if (onnxruntime_BUILD_UNIT_TESTS) include(onnxruntime_unittests.cmake) endif() diff --git a/cmake/onnxruntime.cmake b/cmake/onnxruntime.cmake index e77af251bc..85ff320264 100644 --- a/cmake/onnxruntime.cmake +++ b/cmake/onnxruntime.cmake @@ -79,7 +79,6 @@ endif() add_dependencies(onnxruntime onnxruntime_generate_def ${onnxruntime_EXTERNAL_DEPENDENCIES}) target_include_directories(onnxruntime PRIVATE ${ONNXRUNTIME_ROOT}) -onnxruntime_add_include_to_target(onnxruntime) target_compile_definitions(onnxruntime PRIVATE VER_MAJOR=${VERSION_MAJOR_PART}) target_compile_definitions(onnxruntime PRIVATE VER_MINOR=${VERSION_MINOR_PART}) diff --git a/cmake/onnxruntime_common.cmake b/cmake/onnxruntime_common.cmake index 222eab1ec6..6e50a611ce 100644 --- a/cmake/onnxruntime_common.cmake +++ b/cmake/onnxruntime_common.cmake @@ -119,10 +119,9 @@ target_include_directories(onnxruntime_common PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${ONNXRUNTIME_ROOT} ${eigen_INCLUDE_DIRS} # propagate include directories of dependencies that are part of public interface PUBLIC - $ ${OPTIONAL_LITE_INCLUDE_DIR}) -target_link_libraries(onnxruntime_common Boost::mp11) +target_link_libraries(onnxruntime_common safeint_interface Boost::mp11) if(NOT WIN32) target_include_directories(onnxruntime_common PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/external/nsync/public") diff --git a/cmake/onnxruntime_objectivec.cmake b/cmake/onnxruntime_objectivec.cmake new file mode 100644 index 0000000000..e817eaf069 --- /dev/null +++ b/cmake/onnxruntime_objectivec.cmake @@ -0,0 +1,124 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +if(${CMAKE_VERSION} VERSION_LESS "3.18") + message(FATAL_ERROR "CMake 3.18+ is required when building the Objective-C API.") +endif() + +check_language(OBJC) +if(CMAKE_OBJC_COMPILER) + enable_language(OBJC) +else() + message(FATAL_ERROR "Objective-C is not supported.") +endif() + +check_language(OBJCXX) +if(CMAKE_OBJCXX_COMPILER) + enable_language(OBJCXX) +else() + message(FATAL_ERROR "Objective-C++ is not supported.") +endif() + +add_compile_options( + "$<$:-Wall>" + "$<$:-Wextra>") +if(onnxruntime_DEV_MODE) + add_compile_options( + "$<$:-Werror>") +endif() + +set(OBJC_ROOT "${REPO_ROOT}/objectivec") + +set(OBJC_ARC_COMPILE_OPTIONS "-fobjc-arc" "-fobjc-arc-exceptions") + +# onnxruntime_objc target + +# these headers are the public interface +# explicitly list them here so it is easy to see what is included +set(onnxruntime_objc_headers + "${OBJC_ROOT}/include/onnxruntime.h" + "${OBJC_ROOT}/include/onnxruntime/ort_env.h" + "${OBJC_ROOT}/include/onnxruntime/ort_session.h" + "${OBJC_ROOT}/include/onnxruntime/ort_value.h") + +file(GLOB onnxruntime_objc_srcs + "${OBJC_ROOT}/src/*.h" + "${OBJC_ROOT}/src/*.m" + "${OBJC_ROOT}/src/*.mm") + +# files common to implementation and test targets +set(onnxruntime_objc_common_srcs + "${OBJC_ROOT}/common/assert_arc_enabled.mm") + +source_group(TREE "${OBJC_ROOT}" FILES + ${onnxruntime_objc_headers} + ${onnxruntime_objc_srcs} + ${onnxruntime_objc_common_srcs}) + +add_library(onnxruntime_objc SHARED + ${onnxruntime_objc_headers} + ${onnxruntime_objc_srcs} + ${onnxruntime_objc_common_srcs}) + +target_include_directories(onnxruntime_objc + PUBLIC + "${OBJC_ROOT}/include" + PRIVATE + "${ONNXRUNTIME_ROOT}" + "${OBJC_ROOT}") + +find_library(FOUNDATION_LIB Foundation REQUIRED) + +target_link_libraries(onnxruntime_objc + PRIVATE + onnxruntime + safeint_interface + ${FOUNDATION_LIB}) + +target_compile_options(onnxruntime_objc PRIVATE ${OBJC_ARC_COMPILE_OPTIONS}) + +set_target_properties(onnxruntime_objc PROPERTIES + FRAMEWORK TRUE + VERSION "1.0.0" + SOVERSION "1.0.0" + FRAMEWORK_VERSION "A" + PUBLIC_HEADER "${onnxruntime_objc_headers}" + FOLDER "ONNXRuntime" + CXX_STANDARD 17) # TODO remove when everything else moves to 17 + +if(onnxruntime_BUILD_UNIT_TESTS) + find_package(XCTest REQUIRED) + + # onnxruntime_test_objc target + + file(GLOB onnxruntime_objc_test_srcs + "${OBJC_ROOT}/test/*.h" + "${OBJC_ROOT}/test/*.m" + "${OBJC_ROOT}/test/*.mm") + + source_group(TREE "${OBJC_ROOT}" FILES ${onnxruntime_objc_test_srcs}) + + xctest_add_bundle(onnxruntime_objc_test onnxruntime_objc + ${onnxruntime_objc_headers} + ${onnxruntime_objc_test_srcs} + ${onnxruntime_objc_common_srcs}) + + target_include_directories(onnxruntime_objc_test + PRIVATE + "${OBJC_ROOT}") + + target_compile_options(onnxruntime_objc_test PRIVATE ${OBJC_ARC_COMPILE_OPTIONS}) + + set_target_properties(onnxruntime_objc_test PROPERTIES + FOLDER "ONNXRuntimeTest") + + add_custom_command(TARGET onnxruntime_objc_test POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${OBJC_ROOT}/test/testdata" + "$/Resources/testdata") + + xctest_add_test(XCTest.onnxruntime_objc_test onnxruntime_objc_test) + + set_property(TEST XCTest.onnxruntime_objc_test APPEND PROPERTY + ENVIRONMENT "DYLD_LIBRARY_PATH=$") +endif() diff --git a/cmake/onnxruntime_providers.cmake b/cmake/onnxruntime_providers.cmake index d48d853297..edf543f7a8 100644 --- a/cmake/onnxruntime_providers.cmake +++ b/cmake/onnxruntime_providers.cmake @@ -1041,7 +1041,7 @@ if (onnxruntime_USE_ROCM) endif() # During transition to separate hipFFT repo, put hipfft/include early target_include_directories(onnxruntime_providers_rocm PRIVATE ${onnxruntime_ROCM_HOME}/hipfft/include ${onnxruntime_ROCM_HOME}/include ${onnxruntime_ROCM_HOME}/hipcub/include ${onnxruntime_ROCM_HOME}/hiprand/include ${onnxruntime_ROCM_HOME}/rocrand/include) - target_include_directories(onnxruntime_providers_rocm PRIVATE ${ONNXRUNTIME_ROOT} ${CMAKE_CURRENT_BINARY_DIR}/amdgpu/onnxruntime ${MPI_INCLUDE_DIRS} ${SAFEINT_INCLUDE_DIR} ${ONNXRUNTIME_ROOT}/../cmake/external/eigen) + target_include_directories(onnxruntime_providers_rocm PRIVATE ${ONNXRUNTIME_ROOT} ${CMAKE_CURRENT_BINARY_DIR}/amdgpu/onnxruntime ${MPI_INCLUDE_DIRS} ${ONNXRUNTIME_ROOT}/../cmake/external/eigen) if (onnxruntime_ENABLE_TRAINING) target_include_directories(onnxruntime_providers_rocm PRIVATE ${ORTTRAINING_ROOT} ${CMAKE_CURRENT_BINARY_DIR}/amdgpu/orttraining) diff --git a/docs/Coding_Conventions_and_Standards.md b/docs/Coding_Conventions_and_Standards.md index 2b2bb3830c..46985f9217 100644 --- a/docs/Coding_Conventions_and_Standards.md +++ b/docs/Coding_Conventions_and_Standards.md @@ -70,3 +70,8 @@ The [autopep8](https://pypi.org/project/autopep8/) tool can be used to automatic Editors such as PyCharm [(see here)](https://www.jetbrains.com/help/pycharm/code-inspection.html) and Visual Studio Code [(see here)](https://code.visualstudio.com/docs/python/linting#_flake8) can be configured to check for PEP8 issues. +## Objective-C/C++ Code Style + +Please follow the [Google Objective-C/C++ Style Guide](https://google.github.io/styleguide/objcguide.html). + +Clang-format can be used to format Objective-C/C++ code. The .clang-format file is in the repository root directory. diff --git a/include/onnxruntime/core/session/onnxruntime_c_api.h b/include/onnxruntime/core/session/onnxruntime_c_api.h index faca901ce9..0e6e51062a 100644 --- a/include/onnxruntime/core/session/onnxruntime_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_c_api.h @@ -157,7 +157,7 @@ ORT_RUNTIME_CLASS(Env); ORT_RUNTIME_CLASS(Status); // nullptr for Status* indicates success ORT_RUNTIME_CLASS(MemoryInfo); ORT_RUNTIME_CLASS(IoBinding); -ORT_RUNTIME_CLASS(Session); //Don't call OrtReleaseSession from Dllmain (because session owns a thread pool) +ORT_RUNTIME_CLASS(Session); //Don't call ReleaseSession from Dllmain (because session owns a thread pool) ORT_RUNTIME_CLASS(Value); ORT_RUNTIME_CLASS(RunOptions); ORT_RUNTIME_CLASS(TypeInfo); @@ -343,12 +343,12 @@ struct OrtApi { const char*(ORT_API_CALL* GetErrorMessage)(_In_ const OrtStatus* status)NO_EXCEPTION ORT_ALL_ARGS_NONNULL; /** - * \param out Should be freed by `OrtReleaseEnv` after use + * \param out Should be freed by `ReleaseEnv` after use */ ORT_API2_STATUS(CreateEnv, OrtLoggingLevel logging_level, _In_ const char* logid, _Outptr_ OrtEnv** out); /** - * \param out Should be freed by `OrtReleaseEnv` after use + * \param out Should be freed by `ReleaseEnv` after use */ ORT_API2_STATUS(CreateEnvWithCustomLogger, OrtLoggingFunction logging_function, _In_opt_ void* logger_param, OrtLoggingLevel logging_level, _In_ const char* logid, _Outptr_ OrtEnv** out); @@ -375,7 +375,7 @@ struct OrtApi { _Inout_updates_all_(output_names_len) OrtValue** output); /** - * \return A pointer of the newly created object. The pointer should be freed by OrtReleaseSessionOptions after use + * \return A pointer of the newly created object. The pointer should be freed by ReleaseSessionOptions after use */ ORT_API2_STATUS(CreateSessionOptions, _Outptr_ OrtSessionOptions** options); @@ -432,7 +432,7 @@ struct OrtApi { ORT_API2_STATUS(SetInterOpNumThreads, _Inout_ OrtSessionOptions* options, int inter_op_num_threads); /* - Create a custom op domain. After all sessions using it are released, call OrtReleaseCustomOpDomain + Create a custom op domain. After all sessions using it are released, call ReleaseCustomOpDomain */ ORT_API2_STATUS(CreateCustomOpDomain, _In_ const char* domain, _Outptr_ OrtCustomOpDomain** out); @@ -474,18 +474,18 @@ struct OrtApi { ORT_API2_STATUS(SessionGetOverridableInitializerCount, _In_ const OrtSession* sess, _Out_ size_t* out); /** - * \param out should be freed by OrtReleaseTypeInfo after use + * \param out should be freed by ReleaseTypeInfo after use */ ORT_API2_STATUS(SessionGetInputTypeInfo, _In_ const OrtSession* sess, size_t index, _Outptr_ OrtTypeInfo** type_info); /** - * \param out should be freed by OrtReleaseTypeInfo after use + * \param out should be freed by ReleaseTypeInfo after use */ ORT_API2_STATUS(SessionGetOutputTypeInfo, _In_ const OrtSession* sess, size_t index, _Outptr_ OrtTypeInfo** type_info); /** - * \param out should be freed by OrtReleaseTypeInfo after use + * \param out should be freed by ReleaseTypeInfo after use */ ORT_API2_STATUS(SessionGetOverridableInitializerTypeInfo, _In_ const OrtSession* sess, size_t index, _Outptr_ OrtTypeInfo** type_info); @@ -501,7 +501,7 @@ struct OrtApi { _Inout_ OrtAllocator* allocator, _Outptr_ char** value); /** - * \return A pointer to the newly created object. The pointer should be freed by OrtReleaseRunOptions after use + * \return A pointer to the newly created object. The pointer should be freed by ReleaseRunOptions after use */ ORT_API2_STATUS(CreateRunOptions, _Outptr_ OrtRunOptions** out); @@ -520,8 +520,8 @@ struct OrtApi { ORT_API2_STATUS(RunOptionsUnsetTerminate, _Inout_ OrtRunOptions* options); /** - * Create a tensor from an allocator. OrtReleaseValue will also release the buffer inside the output value - * \param out Should be freed by calling OrtReleaseValue + * Create a tensor from an allocator. ReleaseValue will also release the buffer inside the output value + * \param out Should be freed by calling ReleaseValue * \param type must be one of TENSOR_ELEMENT_DATA_TYPE_xxxx */ ORT_API2_STATUS(CreateTensorAsOrtValue, _Inout_ OrtAllocator* allocator, _In_ const int64_t* shape, size_t shape_len, @@ -529,8 +529,8 @@ struct OrtApi { /** * Create a tensor with user's buffer. You can fill the buffer either before calling this function or after. - * p_data is owned by caller. OrtReleaseValue won't release p_data. - * \param out Should be freed by calling OrtReleaseValue + * p_data is owned by caller. ReleaseValue won't release p_data. + * \param out Should be freed by calling ReleaseValue */ ORT_API2_STATUS(CreateTensorWithDataAsOrtValue, _In_ const OrtMemoryInfo* info, _Inout_ void* p_data, size_t p_data_len, _In_ const int64_t* shape, size_t shape_len, ONNXTensorElementDataType type, @@ -578,7 +578,7 @@ struct OrtApi { ORT_API2_STATUS(GetOnnxTypeFromTypeInfo, _In_ const OrtTypeInfo*, _Out_ enum ONNXType* out); /** - * The 'out' value should be released by calling OrtReleaseTensorTypeAndShapeInfo + * The 'out' value should be released by calling ReleaseTensorTypeAndShapeInfo */ ORT_API2_STATUS(CreateTensorTypeAndShapeInfo, _Outptr_ OrtTensorTypeAndShapeInfo** out); @@ -611,14 +611,14 @@ struct OrtApi { ORT_API2_STATUS(GetTensorShapeElementCount, _In_ const OrtTensorTypeAndShapeInfo* info, _Out_ size_t* out); /** - * \param out Should be freed by OrtReleaseTensorTypeAndShapeInfo after use + * \param out Should be freed by ReleaseTensorTypeAndShapeInfo after use */ ORT_API2_STATUS(GetTensorTypeAndShape, _In_ const OrtValue* value, _Outptr_ OrtTensorTypeAndShapeInfo** out); /** * Get the type information of an OrtValue * \param value - * \param out The returned value should be freed by OrtReleaseTypeInfo after use + * \param out The returned value should be freed by ReleaseTypeInfo after use */ ORT_API2_STATUS(GetTypeInfo, _In_ const OrtValue* value, _Outptr_result_maybenull_ OrtTypeInfo** out); @@ -793,7 +793,7 @@ struct OrtApi { ORT_CLASS_RELEASE(Env); ORT_CLASS_RELEASE(Status); // nullptr for Status* indicates success ORT_CLASS_RELEASE(MemoryInfo); - ORT_CLASS_RELEASE(Session); //Don't call OrtReleaseSession from Dllmain (because session owns a thread pool) + ORT_CLASS_RELEASE(Session); //Don't call ReleaseSession from Dllmain (because session owns a thread pool) ORT_CLASS_RELEASE(Value); ORT_CLASS_RELEASE(RunOptions); ORT_CLASS_RELEASE(TypeInfo); @@ -1156,7 +1156,7 @@ struct OrtApi { * Use this in conjunction with DisablePerSessionThreads API or else the session will use * its own thread pools. * - * \param out should be freed by `OrtReleaseEnv` after use + * \param out should be freed by `ReleaseEnv` after use */ ORT_API2_STATUS(CreateEnvWithCustomLoggerAndGlobalThreadPools, OrtLoggingFunction logging_function, _In_opt_ void* logger_param, OrtLoggingLevel logging_level, _In_ const char* logid, _In_ const struct OrtThreadingOptions* tp_options, _Outptr_ OrtEnv** out); diff --git a/objectivec/common/assert_arc_enabled.mm b/objectivec/common/assert_arc_enabled.mm new file mode 100644 index 0000000000..9aa0badb98 --- /dev/null +++ b/objectivec/common/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/format_objc.sh b/objectivec/format_objc.sh new file mode 100755 index 0000000000..059ae0f5db --- /dev/null +++ b/objectivec/format_objc.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# formats Objective-C/C++ code + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +clang-format -i $(find ${SCRIPT_DIR} -name "*.h" -o -name "*.m" -o -name "*.mm") + diff --git a/objectivec/include/onnxruntime.h b/objectivec/include/onnxruntime.h new file mode 100644 index 0000000000..96948f6c4f --- /dev/null +++ b/objectivec/include/onnxruntime.h @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// this header contains the entire ONNX Runtime Objective-C API +// the headers below can also be imported individually + +#import "onnxruntime/ort_env.h" +#import "onnxruntime/ort_session.h" +#import "onnxruntime/ort_value.h" diff --git a/objectivec/include/onnxruntime/ort_env.h b/objectivec/include/onnxruntime/ort_env.h new file mode 100644 index 0000000000..60264f24d0 --- /dev/null +++ b/objectivec/include/onnxruntime/ort_env.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The ORT environment. + */ +@interface ORTEnv : NSObject + +- (nullable instancetype)init NS_UNAVAILABLE; + +/** + * Creates an ORT Environment. + * + * @param[out] error Optional error information set if an error occurs. + * @return The instance, or nil if an error occurs. + */ +- (nullable instancetype)initWithError:(NSError**)error NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/include/onnxruntime/ort_session.h b/objectivec/include/onnxruntime/ort_session.h new file mode 100644 index 0000000000..4aa975c92c --- /dev/null +++ b/objectivec/include/onnxruntime/ort_session.h @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ORTEnv; +@class ORTValue; + +/** + * An ORT session loads and runs a model. + */ +@interface ORTSession : NSObject + +- (nullable instancetype)init NS_UNAVAILABLE; + +/** + * Creates an ORT Session. + * + * @param env The ORT Environment instance. + * @param path The path to the ONNX model. + * @param[out] error Optional error information set if an error occurs. + * @return The instance, or nil if an error occurs. + */ +- (nullable instancetype)initWithEnv:(ORTEnv*)env + modelPath:(NSString*)path + error:(NSError**)error NS_DESIGNATED_INITIALIZER; + +/** + * Runs the model. + * The inputs and outputs are pre-allocated. + * + * @param inputs Dictionary of input names to input ORT values. + * @param outputs Dictionary of output names to output ORT values. + * @param[out] error Optional error information set if an error occurs. + * @return Whether the model was run successfully. + */ +- (BOOL)runWithInputs:(NSDictionary*)inputs + outputs:(NSDictionary*)outputs + error:(NSError**)error; + +/** + * Runs the model. + * The inputs are pre-allocated and the outputs are allocated by ORT. + * + * @param inputs Dictionary of input names to input ORT values. + * @param outputNames Set of output names. + * @param[out] error Optional error information set if an error occurs. + * @return A dictionary of output names to output ORT values with the outputs + * requested in `outputNames`, or nil if an error occurs. + */ +- (nullable NSDictionary*)runWithInputs:(NSDictionary*)inputs + outputNames:(NSSet*)outputNames + error:(NSError**)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/include/onnxruntime/ort_value.h b/objectivec/include/onnxruntime/ort_value.h new file mode 100644 index 0000000000..86e7b28a82 --- /dev/null +++ b/objectivec/include/onnxruntime/ort_value.h @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The supported ORT value types. + */ +typedef NS_ENUM(int32_t, ORTValueType) { + ORTValueTypeUnknown, + ORTValueTypeTensor, +}; + +/** + * The supported ORT tensor element data types. + */ +typedef NS_ENUM(int32_t, ORTTensorElementDataType) { + ORTTensorElementDataTypeUndefined, + ORTTensorElementDataTypeFloat, + ORTTensorElementDataTypeInt32, +}; + +/** + * An ORT value encapsulates data used as an input or output to a model at runtime. + */ +@interface ORTValue : NSObject + +- (nullable instancetype)init NS_UNAVAILABLE; + +/** + * Creates a value that is a tensor. + * The tensor data is allocated by the caller. + * + * @param data The tensor data. + * @param type The tensor data element type. + * @param shape The tensor shape. + * @param[out] error Optional error information set if an error occurs. + * @return The instance, or nil if an error occurs. + */ +- (nullable instancetype)initTensorWithData:(NSMutableData*)tensorData + elementType:(ORTTensorElementDataType)elementType + shape:(NSArray*)shape + error:(NSError**)error; + +/** + * Gets the value type. + * + * @param[out] valueType The type of the value. + * @param[out] error Optional error information set if an error occurs. + * @return Whether the value type was retrieved successfully. + */ +- (BOOL)valueType:(ORTValueType*)valueType + error:(NSError**)error; + +/** + * Gets the tensor data element type. + * This assumes that the value is a tensor. + * + * @param[out] elementType The type of the tensor's data elements. + * @param[out] error Optional error information set if an error occurs. + * @return Whether the tensor data element type was retrieved successfully. + */ +- (BOOL)tensorElementType:(ORTTensorElementDataType*)elementType + error:(NSError**)error; + +/** + * Gets the tensor shape. + * This assumes that the value is a tensor. + * + * @param[out] error Optional error information set if an error occurs. + * @return The tensor shape, or nil if an error occurs. + */ +- (nullable NSArray*)tensorShapeWithError:(NSError**)error; + +/** + * Gets the tensor data. + * This assumes that the value is a tensor. + * + * @param[out] error Optional error information set if an error occurs. + * @return The tensor data, or nil if an error occurs. + */ +- (nullable NSMutableData*)tensorDataWithError:(NSError**)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/src/error_utils.h b/objectivec/src/error_utils.h new file mode 100644 index 0000000000..4e39f71091 --- /dev/null +++ b/objectivec/src/error_utils.h @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#include "core/session/onnxruntime_cxx_api.h" + +NS_ASSUME_NONNULL_BEGIN + +void ORTSaveCodeAndDescriptionToError(int code, const char* description, NSError** error); +void ORTSaveExceptionToError(const Ort::Exception& e, NSError** error); + +NS_ASSUME_NONNULL_END diff --git a/objectivec/src/error_utils.mm b/objectivec/src/error_utils.mm new file mode 100644 index 0000000000..ae1124528a --- /dev/null +++ b/objectivec/src/error_utils.mm @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "src/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 ORTSaveExceptionToError(const Ort::Exception& e, NSError** error) { + ORTSaveCodeAndDescriptionToError(e.GetOrtErrorCode(), e.what(), error); +} + +NS_ASSUME_NONNULL_END diff --git a/objectivec/src/ort_env.mm b/objectivec/src/ort_env.mm new file mode 100644 index 0000000000..097ba15010 --- /dev/null +++ b/objectivec/src/ort_env.mm @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "src/ort_env_internal.h" + +#include + +#include "core/session/onnxruntime_cxx_api.h" + +#import "src/error_utils.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation ORTEnv { + std::optional _env; +} + +- (nullable instancetype)initWithError:(NSError**)error { + self = [super init]; + if (self) { + try { + _env = Ort::Env{}; + } catch (const Ort::Exception& e) { + ORTSaveExceptionToError(e, error); + self = nil; + } + } + return self; +} + +- (Ort::Env&)CXXAPIOrtEnv { + return *_env; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/src/ort_env_internal.h b/objectivec/src/ort_env_internal.h new file mode 100644 index 0000000000..eab3635409 --- /dev/null +++ b/objectivec/src/ort_env_internal.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "onnxruntime/ort_env.h" + +#include "core/session/onnxruntime_cxx_api.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTEnv () + +- (Ort::Env&)CXXAPIOrtEnv; + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/src/ort_session.mm b/objectivec/src/ort_session.mm new file mode 100644 index 0000000000..dcceff1ebf --- /dev/null +++ b/objectivec/src/ort_session.mm @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "onnxruntime/ort_session.h" + +#include +#include + +#include "core/session/onnxruntime_cxx_api.h" + +#import "src/error_utils.h" +#import "src/ort_env_internal.h" +#import "src/ort_value_internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation ORTSession { + std::optional _session; +} + +- (nullable instancetype)initWithEnv:(ORTEnv*)env + modelPath:(NSString*)path + error:(NSError**)error { + self = [super init]; + if (self) { + try { + Ort::SessionOptions sessionOptions{}; // TODO make configurable + _session = Ort::Session{[env CXXAPIOrtEnv], path.UTF8String, sessionOptions}; + } catch (const Ort::Exception& e) { + ORTSaveExceptionToError(e, error); + self = nil; + } + } + return self; +} + +- (BOOL)runWithInputs:(NSDictionary*)inputs + outputs:(NSDictionary*)outputs + error:(NSError**)error { + BOOL status = NO; + try { + Ort::RunOptions runOptions{}; // TODO make configurable + + 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, + inputNames.data(), inputValues.data(), inputNames.size(), + outputNames.data(), outputNames.size(), outputValues.data())); + + status = YES; + } catch (const Ort::Exception& e) { + ORTSaveExceptionToError(e, error); + } + return status; +} + +- (nullable NSDictionary*)runWithInputs:(NSDictionary*)inputs + outputNames:(NSSet*)outputNameSet + error:(NSError**)error { + try { + NSArray* outputNameArray = outputNameSet.allObjects; + + Ort::RunOptions runOptions{}; // TODO make configurable + + 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, + 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; + } catch (const Ort::Exception& e) { + ORTSaveExceptionToError(e, error); + return nil; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/src/ort_value.mm b/objectivec/src/ort_value.mm new file mode 100644 index 0000000000..118cd1bdc8 --- /dev/null +++ b/objectivec/src/ort_value.mm @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "src/ort_value_internal.h" + +#include +#include + +#include "safeint/SafeInt.hpp" + +#include "core/session/onnxruntime_cxx_api.h" + +#import "src/error_utils.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace { +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)}, + {ORTTensorElementDataTypeInt32, ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32, sizeof(int32_t)}, +}; + +ORTValueType CAPIToPublicValueType(ONNXType capi_type) { + const auto it = std::find_if( + std::begin(kValueTypeInfos), std::end(kValueTypeInfos), + [capi_type](const auto& type_info) { return type_info.capi_type == capi_type; }); + if (it == std::end(kValueTypeInfos)) { + throw Ort::Exception{"unsupported value type", ORT_NOT_IMPLEMENTED}; + } + return it->type; +} + +ONNXTensorElementDataType PublicToCAPITensorElementType(ORTTensorElementDataType type) { + const auto it = std::find_if( + std::begin(kElementTypeInfos), std::end(kElementTypeInfos), + [type](const auto& type_info) { return type_info.type == type; }); + if (it == std::end(kElementTypeInfos)) { + throw Ort::Exception{"unsupported tensor element type", ORT_NOT_IMPLEMENTED}; + } + return it->capi_type; +} + +ORTTensorElementDataType CAPIToPublicTensorElementType(ONNXTensorElementDataType capi_type) { + const auto it = std::find_if( + std::begin(kElementTypeInfos), std::end(kElementTypeInfos), + [capi_type](const auto& type_info) { return type_info.capi_type == capi_type; }); + if (it == std::end(kElementTypeInfos)) { + throw Ort::Exception{"unsupported tensor element type", ORT_NOT_IMPLEMENTED}; + } + return it->type; +} + +size_t SizeOfCAPITensorElementType(ONNXTensorElementDataType capi_type) { + const auto it = std::find_if( + std::begin(kElementTypeInfos), std::end(kElementTypeInfos), + [capi_type](const auto& type_info) { return type_info.capi_type == capi_type; }); + if (it == std::end(kElementTypeInfos)) { + throw Ort::Exception{"unsupported tensor element type", ORT_NOT_IMPLEMENTED}; + } + return it->element_size; +} +} + +@interface ORTValue () + +// pointer to any external tensor data to keep alive for the lifetime of the ORTValue +@property(nullable) NSMutableData* externalTensorData; + +@end + +@implementation ORTValue { + std::optional _value; + std::optional _typeInfo; +} + +#pragma mark Public + +- (nullable instancetype)initTensorWithData:(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); + + self = [self initWithCAPIOrtValue:ortValue.release() + externalTensorData:tensorData + error:error]; + } catch (const Ort::Exception& e) { + ORTSaveExceptionToError(e, error); + self = nil; + } + + return self; +} + +- (BOOL)valueType:(ORTValueType*)valueType + error:(NSError**)error { + try { + const auto ortValueType = _typeInfo->GetONNXType(); + *valueType = CAPIToPublicValueType(ortValueType); + return YES; + } catch (const Ort::Exception& e) { + ORTSaveExceptionToError(e, error); + return NO; + } +} + +- (BOOL)tensorElementType:(ORTTensorElementDataType*)elementType + error:(NSError**)error { + try { + const auto tensorTypeAndShapeInfo = _typeInfo->GetTensorTypeAndShapeInfo(); + const auto ortElementType = tensorTypeAndShapeInfo.GetElementType(); + *elementType = CAPIToPublicTensorElementType(ortElementType); + return YES; + } catch (const Ort::Exception& e) { + ORTSaveExceptionToError(e, error); + return NO; + } +} + +- (nullable NSArray*)tensorShapeWithError:(NSError**)error { + try { + const auto tensorTypeAndShapeInfo = _typeInfo->GetTensorTypeAndShapeInfo(); + const std::vector shape = tensorTypeAndShapeInfo.GetShape(); + NSMutableArray* shapeArray = [[NSMutableArray alloc] initWithCapacity:shape.size()]; + for (size_t i = 0; i < shape.size(); ++i) { + shapeArray[i] = @(shape[i]); + } + return shapeArray; + } catch (const Ort::Exception& e) { + ORTSaveExceptionToError(e, error); + return nil; + } +} + +- (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)) { + throw Ort::Exception{"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]; + } catch (const Ort::Exception& e) { + ORTSaveExceptionToError(e, error); + return nil; + } +} + +#pragma mark Internal + +- (nullable instancetype)initWithCAPIOrtValue:(OrtValue*)CAPIOrtValue + externalTensorData:(nullable NSMutableData*)externalTensorData + error:(NSError**)error { + self = [super init]; + if (self) { + try { + _value = Ort::Value{CAPIOrtValue}; + _typeInfo = _value->GetTypeInfo(); + _externalTensorData = externalTensorData; + } catch (const Ort::Exception& e) { + ORTSaveExceptionToError(e, error); + self = nil; + } + } + return self; +} + +- (Ort::Value&)CXXAPIOrtValue { + return *_value; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/src/ort_value_internal.h b/objectivec/src/ort_value_internal.h new file mode 100644 index 0000000000..88bfc84579 --- /dev/null +++ b/objectivec/src/ort_value_internal.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "onnxruntime/ort_value.h" + +#include "core/session/onnxruntime_cxx_api.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTValue () + +- (nullable instancetype)initWithCAPIOrtValue:(OrtValue*)CAPIOrtValue + externalTensorData:(nullable NSMutableData*)externalTensorData + error:(NSError**)error NS_DESIGNATED_INITIALIZER; + +- (Ort::Value&)CXXAPIOrtValue; + +@end + +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 0000000000..8128420aff --- /dev/null +++ b/objectivec/test/ort_env_test.mm @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "onnxruntime/ort_env.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTEnvTest : XCTestCase +@end + +@implementation ORTEnvTest + +- (void)testInitOk { + NSError* err = nil; + ORTEnv* env = [[ORTEnv alloc] initWithError:&err]; + XCTAssertNotNil(env); + XCTAssertNil(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 0000000000..23059ee4dc --- /dev/null +++ b/objectivec/test/ort_session_test.mm @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "onnxruntime/ort_env.h" +#import "onnxruntime/ort_session.h" +#import "onnxruntime/ort_value.h" + +#include + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTSessionTest : XCTestCase + +@property(readonly, nullable) ORTEnv* ortEnv; + +@end + +@implementation ORTSessionTest + +- (void)setUp { + [super setUp]; + + self.continueAfterFailure = NO; + + _ortEnv = [[ORTEnv alloc] initWithError:nil]; + XCTAssertNotNil(_ortEnv); +} + +- (void)tearDown { + _ortEnv = nil; + + [super tearDown]; +} + ++ (NSString*)getTestDataWithRelativePath:(NSString*)relativePath { + NSString* testDataDir = [NSString stringWithFormat:@"%@/Contents/Resources/testdata", + [[NSBundle bundleForClass:[ORTSessionTest class]] bundlePath]]; + return [testDataDir stringByAppendingString:relativePath]; +} + +// model with an Add op +// inputs: A, B +// output: C = A + B ++ (NSString*)getAddModelPath { + return [ORTSessionTest getTestDataWithRelativePath:@"/single_add.onnx"]; +} + ++ (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* ort_value = [[ORTValue alloc] initTensorWithData:data + elementType:ORTTensorElementDataTypeFloat + shape:shape + error:&err]; + XCTAssertNotNil(ort_value); + XCTAssertNil(err); + return ort_value; +} + +- (void)testInitAndRunWithPreallocatedOutputOk { + NSMutableData* a_data = [ORTSessionTest dataWithScalarFloat:1.0f]; + NSMutableData* b_data = [ORTSessionTest dataWithScalarFloat:2.0f]; + NSMutableData* c_data = [ORTSessionTest dataWithScalarFloat:0.0f]; + + ORTValue* a = [ORTSessionTest ortValueWithScalarFloatData:a_data]; + ORTValue* b = [ORTSessionTest ortValueWithScalarFloatData:b_data]; + ORTValue* c = [ORTSessionTest ortValueWithScalarFloatData:c_data]; + + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + error:&err]; + XCTAssertNotNil(session); + XCTAssertNil(err); + + BOOL run_result = [session runWithInputs:@{@"A" : a, @"B" : b} + outputs:@{@"C" : c} + error:&err]; + XCTAssertTrue(run_result); + XCTAssertNil(err); + + const float c_expected = 3.0f; + float c_actual; + memcpy(&c_actual, c_data.bytes, sizeof(float)); + XCTAssertEqual(c_actual, c_expected); +} + +- (void)testInitAndRunOk { + NSMutableData* a_data = [ORTSessionTest dataWithScalarFloat:1.0f]; + NSMutableData* b_data = [ORTSessionTest dataWithScalarFloat:2.0f]; + + ORTValue* a = [ORTSessionTest ortValueWithScalarFloatData:a_data]; + ORTValue* b = [ORTSessionTest ortValueWithScalarFloatData:b_data]; + + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + error:&err]; + XCTAssertNotNil(session); + XCTAssertNil(err); + + NSDictionary* outputs = + [session runWithInputs:@{@"A" : a, @"B" : b} + outputNames:[NSSet setWithArray:@[ @"C" ]] + error:&err]; + XCTAssertNotNil(outputs); + XCTAssertNil(err); + + ORTValue* c_output = outputs[@"C"]; + XCTAssertNotNil(c_output); + + NSData* c_data = [c_output tensorDataWithError:&err]; + XCTAssertNotNil(c_data); + XCTAssertNil(err); + + const float c_expected = 3.0f; + float c_actual; + memcpy(&c_actual, c_data.bytes, sizeof(float)); + XCTAssertEqual(c_actual, c_expected); +} + +- (void)testInitFailsWithInvalidPath { + NSString* invalid_model_path = [ORTSessionTest getTestDataWithRelativePath:@"/invalid/path/to/model.onnx"]; + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:invalid_model_path + error:&err]; + XCTAssertNil(session); + XCTAssertNotNil(err); +} + +- (void)testRunFailsWithInvalidInput { + NSMutableData* d_data = [ORTSessionTest dataWithScalarFloat:1.0f]; + NSMutableData* c_data = [ORTSessionTest dataWithScalarFloat:0.0f]; + + ORTValue* d = [ORTSessionTest ortValueWithScalarFloatData:d_data]; + ORTValue* c = [ORTSessionTest ortValueWithScalarFloatData:c_data]; + + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + error:&err]; + XCTAssertNotNil(session); + XCTAssertNil(err); + + BOOL run_result = [session runWithInputs:@{@"D" : d} + outputs:@{@"C" : c} + error:&err]; + XCTAssertFalse(run_result); + XCTAssertNotNil(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 0000000000..820c30ce49 --- /dev/null +++ b/objectivec/test/ort_value_test.mm @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "onnxruntime/ort_value.h" + +#include + +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 ]; + + NSError* err = nil; + ORTValue* ortValue = [[ORTValue alloc] initTensorWithData:data + elementType:ORTTensorElementDataTypeInt32 + shape:shape + error:&err]; + XCTAssertNotNil(ortValue); + XCTAssertNil(err); + + ORTValueType actualValueType; + XCTAssertTrue([ortValue valueType:&actualValueType error:&err]); + XCTAssertNil(err); + XCTAssertEqual(actualValueType, ORTValueTypeTensor); + + ORTTensorElementDataType actualElementType; + XCTAssertTrue([ortValue tensorElementType:&actualElementType error:&err]); + XCTAssertNil(err); + XCTAssertEqual(actualElementType, ORTTensorElementDataTypeInt32); + + NSArray* actualShape = [ortValue tensorShapeWithError:&err]; + XCTAssertNotNil(actualShape); + XCTAssertNil(err); + XCTAssertEqualObjects(shape, actualShape); + + NSData* actualData = [ortValue tensorDataWithError:&err]; + XCTAssertNotNil(actualData); + XCTAssertNil(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* ort_value = [[ORTValue alloc] initTensorWithData:data + elementType:ORTTensorElementDataTypeInt32 + shape:shape + error:&err]; + XCTAssertNil(ort_value); + XCTAssertNotNil(err); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/testdata/single_add.onnx b/objectivec/test/testdata/single_add.onnx new file mode 100644 index 0000000000..0939094d38 --- /dev/null +++ b/objectivec/test/testdata/single_add.onnx @@ -0,0 +1,16 @@ +:S + +A +BCAdd"Add SingleAddZ +A + + +Z +B + + +b +C + + +B \ No newline at end of file diff --git a/objectivec/test/testdata/single_add_gen.py b/objectivec/test/testdata/single_add_gen.py new file mode 100644 index 0000000000..f72b8471d4 --- /dev/null +++ b/objectivec/test/testdata/single_add_gen.py @@ -0,0 +1,19 @@ +import onnx +from onnx import helper +from onnx import TensorProto + +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) +onnx.save(model, r'single_add.onnx') diff --git a/onnxruntime/core/framework/tensor_type_and_shape.cc b/onnxruntime/core/framework/tensor_type_and_shape.cc index d00f37f3cb..a5b7308636 100644 --- a/onnxruntime/core/framework/tensor_type_and_shape.cc +++ b/onnxruntime/core/framework/tensor_type_and_shape.cc @@ -1,19 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#include "core/session/onnxruntime_c_api.h" -#include "core/session/ort_apis.h" -#include "core/framework/tensor_shape.h" +#include "core/framework/tensor_type_and_shape.h" + +#include +#include +#include + +#include "core/common/safeint.h" +#include "core/framework/error_code_helper.h" #include "core/framework/ml_value.h" #include "core/framework/onnxruntime_typeinfo.h" #include "core/framework/sparse_tensor.h" -#include "core/framework/tensor_type_and_shape.h" +#include "core/framework/tensor_shape.h" #include "core/graph/onnx_protobuf.h" -#include "core/framework/error_code_helper.h" - -#include -#include -#include +#include "core/session/onnxruntime_c_api.h" +#include "core/session/ort_apis.h" using onnxruntime::BFloat16; using onnxruntime::DataTypeImpl; @@ -72,8 +74,10 @@ ORT_API_STATUS_IMPL(OrtApis::GetSymbolicDimensions, _In_ const struct OrtTensorT } ORT_API_STATUS_IMPL(OrtApis::GetTensorShapeElementCount, _In_ const OrtTensorTypeAndShapeInfo* this_ptr, _Out_ size_t* out) { - *out = static_cast(this_ptr->shape.Size()); + API_IMPL_BEGIN + *out = SafeInt{this_ptr->shape.Size()}; return nullptr; + API_IMPL_END } struct OrtValue; diff --git a/onnxruntime/core/framework/tensor_type_and_shape.h b/onnxruntime/core/framework/tensor_type_and_shape.h index f781160cc6..acfc602be7 100644 --- a/onnxruntime/core/framework/tensor_type_and_shape.h +++ b/onnxruntime/core/framework/tensor_type_and_shape.h @@ -2,6 +2,12 @@ // Licensed under the MIT License. #pragma once +#include +#include + +#include "core/framework/tensor_shape.h" +#include "core/session/onnxruntime_c_api.h" + struct OrtTensorTypeAndShapeInfo { public: ONNXTensorElementDataType type = ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT; diff --git a/tools/ci_build/build.py b/tools/ci_build/build.py index d83a700e0f..6f1405870e 100644 --- a/tools/ci_build/build.py +++ b/tools/ci_build/build.py @@ -243,6 +243,11 @@ def parse_arguments(): "--build_nodejs", action='store_true', help="Build Node.js binding and NPM package.") + # Objective-C binding + parser.add_argument( + "--build_objc", action='store_true', + help="Build Objective-C binding.") + # Build a shared lib parser.add_argument( "--build_shared_lib", action='store_true', @@ -656,6 +661,7 @@ def generate_build_tree(cmake_path, source_dir, build_dir, cuda_home, cudnn_home "-Donnxruntime_BUILD_CSHARP=" + ("ON" if args.build_csharp else "OFF"), "-Donnxruntime_BUILD_JAVA=" + ("ON" if args.build_java else "OFF"), "-Donnxruntime_BUILD_NODEJS=" + ("ON" if args.build_nodejs else "OFF"), + "-Donnxruntime_BUILD_OBJC=" + ("ON" if args.build_objc else "OFF"), "-Donnxruntime_BUILD_SHARED_LIB=" + ("ON" if args.build_shared_lib else "OFF"), "-Donnxruntime_BUILD_APPLE_FRAMEWORK=" + ("ON" if args.build_apple_framework else "OFF"), "-Donnxruntime_USE_DNNL=" + ("ON" if args.use_dnnl else "OFF"), diff --git a/tools/ci_build/github/azure-pipelines/mac-ci-pipeline.yml b/tools/ci_build/github/azure-pipelines/mac-ci-pipeline.yml index d69082065b..35f3b21dfd 100644 --- a/tools/ci_build/github/azure-pipelines/mac-ci-pipeline.yml +++ b/tools/ci_build/github/azure-pipelines/mac-ci-pipeline.yml @@ -2,7 +2,18 @@ jobs: - template: templates/mac-ci.yml parameters: DoNugetPack: 'false' - BuildCommand: 'python3 $(Build.SourcesDirectory)/tools/ci_build/build.py --build_dir $(Build.BinariesDirectory) --build_wheel --skip_submodule_sync --parallel --build_shared_lib --build_java --build_nodejs --enable_language_interop_ops --config Debug' + BuildCommand: >- + python3 $(Build.SourcesDirectory)/tools/ci_build/build.py + --config Debug + --build_dir $(Build.BinariesDirectory) + --build_wheel + --skip_submodule_sync + --parallel + --build_shared_lib + --build_java + --build_nodejs + --build_objc + --enable_language_interop_ops # Enable unreleased onnx opsets in CI builds # This facilitates testing the implementation for the new opsets AllowReleasedOpsetOnly: '0'