Skip to content

Native Objective‑C/Swift Wrapper for the ExecuTorch Tensor #8366

Open
@shoumikhin

Description

@shoumikhin

🚀 The feature, motivation and pitch

ExecuTorchTensor is a lightweight Objective‑C wrapper for the ExecuTorch C++ Tensor API. It provides a natural, efficient interface for creating, accessing, and manipulating tensor data without exposing the C++ complexity. With multiple initializers (from raw bytes, NSData, scalar arrays, or single scalars), it allows users to create tensors in various ways. The wrapper also exposes key metadata - such as data type, shape, dimension order, strides, and element count - in Objective‑C types, which are bridged naturally to Swift. This enables seamless, high‑performance integration into iOS/macOS applications using ExecuTorch.

Alternatives

See the parent issue #8360 for motivation and alternatives overview.

Additional context

  • The wrapper minimizes data copying. For instance, when constructing a tensor from raw bytes, the data is not duplicated.
  • Metadata (shape, strides, dimension order) is cached as NSArrays for efficient repeated access.
  • Advanced functionality (e.g. extended convenience initializers or additional tensor manipulation methods) may be added in future iterations.
  • The nativeInstance accessor exposes a pointer to the underlying C++ tensor (wrapped as a TensorPtr) and is intended for internal use (e.g. when the tensor must be passed to other C++ APIs). This pointer is not exposed to Swift.
  • We also considered the design of the existing Swift interfaces for Tensor abstraction in various frameworks:

RFC (Optional)

The ExecuTorchTensor wrapper provides a robust, multi-faceted Objective‑C interface to manage tensor objects backed by a native C++ TensorPtr. It supports various initialization modes (from raw pointers, NSData, arrays of scalars, or single scalar values), data access (both immutable and mutable), cloning, resizing, and creating patterned tensors (empty, full, ones, zeros, random, random normal, and random integer). Swift interoperability is achieved via NS_SWIFT_NAME annotations, ensuring a natural Swift API.

Internal Representation

The tensor wrapper internally holds a C++ TensorPtr:

::executorch::extension::TensorPtr _tensor;

Additional members include:

  • Cached metadata for shape, strides, and dimensionOrder.

  • An optional NSData reference used when initializing from data.

  • The underlying tensor pointer is exposed through a read‑only property (unavailable in Swift).

  • Properties

The following properties provide read‑only access to tensor metadata:

Objective-C

// The scalar type of the tensor.
@property(nonatomic, readonly) ExecuTorchDataType dataType;

// An array describing the dimensions.
@property(nonatomic, readonly) NSArray<NSNumber *> *shape;

// The order in which dimensions are stored.
@property(nonatomic, readonly) NSArray<NSNumber *> *dimensionOrder;

// An array of strides for each dimension.
@property(nonatomic, readonly) NSArray<NSNumber *> *strides;

// Indicates whether the shape is static or dynamic.
@property(nonatomic, readonly) ExecuTorchShapeDynamism shapeDynamism;

// The total number of elements in the tensor.
@property(nonatomic, readonly) NSInteger count;

// A pointer to the underlying C++ tensor.
@property(nonatomic, readonly) void *nativeInstance NS_SWIFT_UNAVAILABLE("");

Swift

print(tensor.dataType)       // e.g. .float
print(tensor.shape)          // e.g. [2, 3]
print(tensor.dimensionOrder) // e.g. [0, 1]
print(tensor.strides)        // e.g. [3, 1]
print(tensor.shapeDynamism)  // e.g. .dynamicBound
print(tensor.count)          // e.g. 6

Key Methods:

  • Initialization with a C++ Counterpart:

Creates a tensor wrapper from an existing native instance. This is used internally and is unavailable in Swift.

Objective-C

- (instancetype)initWithNativeInstance:(void *)nativeInstance NS_DESIGNATED_INITIALIZER NS_SWIFT_UNAVAILABLE("");
  • Initialization with a Tensor:

Constructs a new tensor by sharing the data that another Tensor already uses, where the metadata like shape can be further modified, which allows creating a "view" on the Tensor's data without modifying the original Tensor.

Objective-C

- (instancetype)initWithTensor:(ExecuTorchTensor *)otherTensor NS_SWIFT_NAME(init(_:));

Swift

let tensor = Tensor(otherTensor)
  • Copying

Tensor should conform to NSCopying and implement corresponding methods:

- (instancetype)copy;
- (instancetype)copyWithZone:(nullable NSZone *)zone;  // Impl only, not exposed to header.

Copy will use clone_tensor_ptr internally to duplicate both the data and metadata that the Tensor owns and return a clone.

  • Data Aceess

The API provides two methods to access the raw tensor data as immutable or mutable data:

Objective-C

- (void)bytesWithHandler:(void (^)(const void *pointer, NSInteger count, ExecuTorchDataType type))handler NS_SWIFT_NAME(bytes(_:));

- (void)mutableBytesWithHandler:(void (^)(void *pointer, NSInteger count, ExecuTorchDataType dataType))handler NS_SWIFT_NAME(mutableBytes(_:));

Swift

tensor.bytes { pointer, count, dataType in
  print("Data type:", dataType)
  print("Element count:", count)
}

tensor.mutableBytes { pointer, count, dataType in
  guard dataType == .float else {
    return
  }
  let floatBuffer = pointer.bindMemory(to: Float.self, capacity: count)
  for i in 0..<count {
    floatBuffer[i] = 0.0
  }
}
  • Resizing

The tensor can be resized by providing a new shape. If the underlying C++ resize function fails, an error is reported using the ExecuTorchErrorDomain.

Objective-C

- (BOOL)resizeToShape:(NSArray<NSNumber *> *)shape error:(NSError **)error NS_SWIFT_NAME(resize(to:));

Swift:

try tensor.resize(to: [4, 4])
  • Equality

Tensors are compared by checking the data type, element count, underlying raw data (via std::memcmp), and metadata (shape, strides, dimension order, and shape dynamism).

Objective-C

- (BOOL)isEqualToTensor:(nullable ExecuTorchTensor *)other;

Swift:

let isEqual = tensor == anotherTensor
  • Initialize with Bytes without Copying

These initializers wrap an external memory pointer without copying the data. Users can optionally supply strides, dimension order, and an explicit shape dynamism.

Objective-C

- (instancetype)initWithBytesNoCopy:(void *)pointer
                              shape:(NSArray<NSNumber *> *)shape
                            strides:(NSArray<NSNumber *> *)strides
                     dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
                           dataType:(ExecuTorchDataType)dataType
                      shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;

- (instancetype)initWithBytesNoCopy:(void *)pointer
                              shape:(NSArray<NSNumber *> *)shape
                            strides:(NSArray<NSNumber *> *)strides
                     dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
                           dataType:(ExecuTorchDataType)dataType;

- (instancetype)initWithBytesNoCopy:(void *)pointer
                              shape:(NSArray<NSNumber *> *)shape
                           dataType:(ExecuTorchDataType)dataType
                      shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;

- (instancetype)initWithBytesNoCopy:(void *)pointer
                              shape:(NSArray<NSNumber *> *)shape
                           dataType:(ExecuTorchDataType)dataType;

Swift:

let tensor = Tensor(bytesNoCopy: pointer, shape: [2, 3], dataType: .float)
  • Initialize with Bytes

These methods copy data from a raw pointer into an internal buffer. The total byte count is computed from the provided shape and data type.

Objective-C

- (instancetype)initWithBytes:(const void *)pointer
                        shape:(NSArray<NSNumber *> *)shape
                      strides:(NSArray<NSNumber *> *)strides
               dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
                     dataType:(ExecuTorchDataType)dataType
                shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;

- (instancetype)initWithBytes:(const void *)pointer
                        shape:(NSArray<NSNumber *> *)shape
                      strides:(NSArray<NSNumber *> *)strides
               dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
                     dataType:(ExecuTorchDataType)dataType;

- (instancetype)initWithBytes:(const void *)pointer
                        shape:(NSArray<NSNumber *> *)shape
                     dataType:(ExecuTorchDataType)dataType
                shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;

- (instancetype)initWithBytes:(const void *)pointer
                        shape:(NSArray<NSNumber *> *)shape
                     dataType:(ExecuTorchDataType)dataType;

Swift

let tensor = Tensor(bytes: pointer, shape: [2, 3], strides: [3, 1], dimensionOrder: [0, 1], dataType: .float)
  • Initialize with Data

Initialize a tensor directly from an NSData instance. The initializer ensures the data buffer is large enough for the given shape and data type.

Objective-C

- (instancetype)initWithData:(NSData *)data
                       shape:(NSArray<NSNumber *> *)shape
                     strides:(NSArray<NSNumber *> *)strides
              dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
                    dataType:(ExecuTorchDataType)dataType
               shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;

- (instancetype)initWithData:(NSData *)data
                       shape:(NSArray<NSNumber *> *)shape
                     strides:(NSArray<NSNumber *> *)strides
              dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
                    dataType:(ExecuTorchDataType)dataType;

- (instancetype)initWithData:(NSData *)data
                       shape:(NSArray<NSNumber *> *)shape
                    dataType:(ExecuTorchDataType)dataType
               shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;

- (instancetype)initWithData:(NSData *)data
                       shape:(NSArray<NSNumber *> *)shape
                    dataType:(ExecuTorchDataType)dataType;

Swift

let tensor = Tensor(data: data, shape: [2, 3], dataType: .float, shapeDynamism: .static)
  • Initialize with an Array of Scalars

These initializers create a tensor from an array of scalar values (NSNumber). The number of scalars must match the element count implied by the shape. Under the hood, each scalar is converted to its corresponding C++ type.

Objective-C

- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars
                          shape:(NSArray<NSNumber *> *)shape
                        strides:(NSArray<NSNumber *> *)strides
                 dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
                       dataType:(ExecuTorchDataType)dataType
                  shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(init(_:shape:strides:dimensionOrder:dataType:shapeDynamism:));

- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars
                          shape:(NSArray<NSNumber *> *)shape
                        strides:(NSArray<NSNumber *> *)strides
                 dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
                       dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(init(_:shape:strides:dimensionOrder:dataType:));

- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars
                          shape:(NSArray<NSNumber *> *)shape
                       dataType:(ExecuTorchDataType)dataType
                  shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(init(_:shape:dataType:shapeDynamism:));

- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars
                          shape:(NSArray<NSNumber *> *)shape
                       dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(init(_:shape:dataType:));

- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars
                       dataType:(ExecuTorchDataType)dataType
                  shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(init(_:dataType:shapeDynamism:));

- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars NS_SWIFT_NAME(init(_:));

Swift

let tensorFromScalars = Tensor([1.0, 2.0, 3.0], shape: [3], dataType: .float, shapeDynamism: .dynamicBound)
  • Initialize with a Scalar

Convenience initializers for single scalar values are provided with overloads for different primitive types. These wrap a single value as an array of one element, using an empty shape.

Objective-C

- (instancetype)initWithScalar:(NSNumber *)scalar
                      dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(init(_:dataType:));

- (instancetype)initWithScalar:(NSNumber *)scalar NS_SWIFT_NAME(init(_:));

- (instancetype)initWithByte:(uint8_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithChar:(int8_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithShort:(int16_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithInt:(int32_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithLong:(int64_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithFloat:(float)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithDouble:(double)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithBool:(BOOL)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithUInt16:(uint16_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithUInt32:(uint32_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithUInt64:(uint64_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithInteger:(NSInteger)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithUnsignedInteger:(NSUInteger)scalar NS_SWIFT_NAME(init(_:));

Swift

let pi = Tensor(3.14)
  • Create an Empty Tensor

Return a tensor without any predefined values. Overloads allow creation based on a given shape or by mirroring an existing tensor’s metadata.

Objective-C

+ (instancetype)emptyTensorWithShape:(NSArray<NSNumber *> *)shape
                             strides:(NSArray<NSNumber *> *)strides
                            dataType:(ExecuTorchDataType)dataType
                       shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(empty(shape:strides:dataType:shapeDynamism:));

+ (instancetype)emptyTensorWithShape:(NSArray<NSNumber *> *)shape
                            dataType:(ExecuTorchDataType)dataType
                       shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(empty(shape:dataType:shapeDynamism:));

+ (instancetype)emptyTensorWithShape:(NSArray<NSNumber *> *)shape
                            dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(empty(shape:dataType:));

+ (instancetype)emptyTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(empty(like:));

Swift

let emptyTensor = Tensor.empty(shape: [2, 3], dataType: .float)
  • Create a Full Tensor

Create tensors filled with a given scalar value. There are overloads to either specify strides explicitly or to mimic an existing tensor’s layout.

Objective-C

+ (instancetype)fullTensorWithShape:(NSArray<NSNumber *> *)shape
                             scalar:(NSNumber *)scalar
                            strides:(NSArray<NSNumber *> *)strides
                           dataType:(ExecuTorchDataType)dataType
                      shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(full(shape:scalar:strides:dataType:shapeDynamism:));

+ (instancetype)fullTensorWithShape:(NSArray<NSNumber *> *)shape
                             scalar:(NSNumber *)scalar
                           dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(full(shape:scalar:dataType:));

+ (instancetype)fullTensorLikeTensor:(ExecuTorchTensor *)tensor
                              scalar:(NSNumber *)scalar NS_SWIFT_NAME(full(like:scalar:));

Swift

let fullTensor = Tensor.full(shape: [2, 3], scalar: 7, dataType: .int)
  • Create a Tensor with Ones and Zeros

These are shortcuts for full tensors with a fill value of 1 or 0, respectively.

Objective-C

+ (instancetype)onesTensorWithShape:(NSArray<NSNumber *> *)shape
                           dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(ones(shape:dataType:));

+ (instancetype)onesTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(ones(like:));

+ (instancetype)zerosTensorWithShape:(NSArray<NSNumber *> *)shape
                            dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(zeros(shape:dataType:));

+ (instancetype)zerosTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(zeros(like:));

Swift

let onesTensor = Tensor.ones(shape: [3, 2], dataType: .float)

let zerosTensor = Tensor.zeros(shape: [2, 3], dataType: .double)
  • Create Random Tensors

These methods create tensors with random values. Separate factory methods support uniform random values, normal distribution and random integers within a specified range.

Objective-C

+ (instancetype)randomTensorWithShape:(NSArray<NSNumber *> *)shape
                             dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(rand(shape:dataType:));

+ (instancetype)randomTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(rand(like:));

+ (instancetype)randomNormalTensorWithShape:(NSArray<NSNumber *> *)shape
                                   dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(randn(shape:dataType:));

+ (instancetype)randomNormalTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(randn(like:));

+ (instancetype)randomIntegerTensorWithLow:(NSInteger)low
                                      high:(NSInteger)high
                                     shape:(NSArray<NSNumber *> *)shape
                                  dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(randint(low:high:shape:dataType:));

+ (instancetype)randomIntegerTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(randint(like:low:high:));

Swift

let randTensor = Tensor.rand(shape: [2, 3], dataType: .float)

let randnTensor = Tensor.randn(shape: [10], dataType: .int)

let randintTensor = Tensor.randint(low: 0, high: 10, shape: [2, 3], dataType: .int)
  • Error Handling & Reporting:

Methods like resizeToShape:error: return a boolean value indicating success. If an error occurs, an NSError is populated using the domain ExecuTorchErrorDomain. Accessor properties are read-only and rely on cached metadata; if such metadata is not available, they return nil or default values, following Objective‑C conventions. In Swift, these methods appear as throwing functions.

Metadata

Metadata

Assignees

Labels

module: iosIssues related to iOS code, build, and executiontriagedThis issue has been looked at a team member, and triaged and prioritized into an appropriate module

Type

No type

Projects

Status

Backlog

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions