Objective-C++ and C Interoperability
|
This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Objective-C is a strict superset of C, so calling C is not "interoperability" at all — it is just calling a function. **Objective-C** goes further: a `.mm` file is compiled as Objective-C *and* C together, so both object models coexist in one translation unit. This is how a cross-platform C++ engine gets a native Cocoa front end.
This page assumes the C and C++ languages themselves and covers only what changes when they meet Objective-C. For the languages in their own right, see the C Reference and the C++ Reference.
Objective-C++
.mm Files
Rename a file from .m to .mm and the compiler accepts C++ syntax as well:
// ImageProcessor.mm
#import "ImageProcessor.h"
#import <vector>
#import <string>
#import <memory>
@implementation ImageProcessor
- (NSArray<NSNumber *> *)histogramForData:(NSData *)data {
std::vector<int> counts(256, 0); // C++ container
const uint8_t *bytes = (const uint8_t *)data.bytes;
for (NSUInteger i = 0; i < data.length; i++) {
counts[bytes[i]]++;
}
NSMutableArray<NSNumber *> *result = [NSMutableArray arrayWithCapacity:counts.size()];
for (int c : counts) { // C++11 range-for
[result addObject:@(c)]; // Objective-C literal
}
return [result copy];
}
@end
Note the mixing: an NSData and a std::vector in one method, Objective-C literals alongside a C++
range-based for.
The compiler flag is -x objective-c++, but the .mm extension selects it automatically. In Xcode, the file
type does the same.
C++ Objects as Instance Variables
A C++ object can be an ivar, but only if the ivar is declared in the .mm — never in the .h, which
must stay importable from ordinary Objective-C:
// ImageProcessor.h -- pure Objective-C, importable from any .m
#import <Foundation/Foundation.h>
@interface ImageProcessor : NSObject
- (NSArray<NSNumber *> *)histogramForData:(NSData *)data;
@end
// ImageProcessor.mm -- C++ lives here, invisible to clients
#import "ImageProcessor.h"
#import <memory>
#import "Engine.hpp"
@implementation ImageProcessor {
std::unique_ptr<Engine> _engine; // C++ member, private to this file
std::vector<float> _weights;
}
- (instancetype)init {
self = [super init];
if (self) {
_engine = std::make_unique<Engine>(); // constructed here
_weights.reserve(1024);
}
return self;
}
- (void)dealloc {
// _engine's destructor runs automatically when the object is destroyed.
// Nothing to write unless the C++ type needs explicit teardown.
}
@end
Three rules govern this:
-
C++ constructors and destructors do run. The compiler calls each ivar’s default constructor after
alloczeroes memory, and its destructor during deallocation — so RAII works. -
A C++ ivar with no default constructor will not compile. Wrap it in a
std::unique_ptrorstd::optional. -
Keep C++ out of the header. This is the "PIMPL at the language boundary" pattern, and it is what lets ordinary
.mfiles use the class without being switched to.mm.
If a header genuinely must expose C++, guard it so a .m file gets a usable declaration:
#ifdef __cplusplus
#include <string>
class Engine;
#endif
@interface ImageProcessor : NSObject
#ifdef __cplusplus
- (void)configureWithEngine:(Engine *)engine;
#endif
@end
C++11 Lambdas versus Blocks
Both are closures; they belong to different object models:
| Block | C++ lambda | |
|---|---|---|
Syntax |
|
|
Capture |
Implicit; objects retained under ARC |
Explicit capture list ( |
Type |
An Objective-C object |
An unnamed class type |
Storage |
|
|
Lifetime |
Reference-counted |
Ordinary C++ value semantics |
Passed to |
Cocoa APIs, GCD |
STL algorithms, C++ APIs |
// A lambda into an STL algorithm:
std::sort(values.begin(), values.end(), [](int a, int b) { return a > b; });
// A block into a Cocoa API:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger i, BOOL *stop) { … }];
// They compose: a block may capture C++ values, and a lambda may call Objective-C.
std::string prefix = "row ";
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"%s%d", prefix.c_str(), n); // the block captured the std::string by value
});
A std::function can hold a block and a block can call a lambda, but be careful with capture-by-reference
([&]) in anything that outlives the current scope — unlike a block’s ARC-managed captures, a dangling
reference is entirely your problem.
ARC and C++ Containers
ARC manages Objective-C pointers; C++ containers manage their elements. Storing one in the other works, but you must be explicit:
// An Objective-C object in a C++ container: ARC-aware in Objective-C++.
std::vector<id> objects;
objects.push_back(someObject); // retained; released when the vector is destroyed
// A C++ object in an Objective-C collection: box it.
auto engine = std::make_shared<Engine>();
NSValue *boxed = [NSValue valueWithPointer:engine.get()]; // the NSValue does NOT own it
// A struct field may be a managed object pointer in both .m and .mm files;
// use __unsafe_unretained only when you intend to manage the lifetime, e.g.
// because the struct is malloc'd and no copy/destroy helper ever runs:
struct Holder {
__unsafe_unretained id object; // you manage the lifetime
};
The strong, weak and __unsafe_unretained qualifiers work on C member variables too. Where the
ownership is genuinely shared across both worlds, the cleanest arrangement is usually to keep the Objective-C
object in an Objective-C property and hand C a plain observer pointer with a documented lifetime.
Calling Plain C
No ceremony at all — C is already the language:
#import <math.h>
#import <stdlib.h>
#import "mylib.h" // a plain C library
- (double)computeFrom:(double)value {
double root = sqrt(value); // libm
char *buffer = (char *)malloc(256); // manual: ARC does not manage this
mylib_format(buffer, 256, root);
NSString *text = [NSString stringWithUTF8String:buffer];
free(buffer); // your responsibility
return root;
}
Two boundaries need attention, both discussed in
Strings, Numbers and Values: strings
(UTF8String returns a borrowed, autoreleased buffer — copy it if it must outlive the statement) and
allocation (malloc/free is entirely yours).
extern "C" and Header Hygiene
C++ mangles function names; C does not. A header that may be included from either must say so, or the linker fails with "undefined symbol" on a function that plainly exists:
// mylib.h -- usable from C, Objective-C, C++ and Objective-C++
#ifndef MYLIB_H
#define MYLIB_H
#ifdef __cplusplus
extern "C" {
#endif
void mylib_init(void);
double mylib_compute(double value);
void mylib_format(char *buffer, size_t size, double value);
#ifdef __cplusplus
}
#endif
#endif /* MYLIB_H */
This idiom is worth applying to every C header you publish. The corresponding rules for your Objective-C headers:
-
Keep public headers free of C++ — otherwise every client file must become
.mm. -
Forward-declare with
@classinstead of importing, where possible. -
Do not import framework umbrella headers from a header that plain C code might include.
-
Prefer Clang modules (
@import) for framework imports; see Modules, Frameworks and Code Organization.
Core Foundation and Toll-Free Bridging
Core Foundation is a C API with its own reference-counting conventions (CFRetain, CFRelease) that ARC does
not manage. Several CF types are the same objects as their Foundation counterparts at run time, so a cast is
all that is needed — but under ARC the cast must state what happens to ownership:
NSString *s = @"Hello";
CFStringRef borrowed = (__bridge CFStringRef)s; // no transfer; do not CFRelease
CFStringRef owned = (__bridge_retained CFStringRef)s; // you own it; CFRelease it
CFRelease(owned);
CFStringRef created = CFStringCreateWithCString(NULL, "hi", kCFStringEncodingUTF8);
NSString *managed = (__bridge_transfer NSString *)created; // ARC owns it now
The decision rule is Core Foundation’s own Create Rule: a function with Create or Copy in its name
returns something you own, so hand it to ARC with bridge_transfer (or CFBridgingRelease). Anything else
is borrowed, and bridge is correct. Full detail is in
Automatic Reference Counting.
Bridged pairs include NSString/CFStringRef, NSArray/CFArrayRef, NSDictionary/CFDictionaryRef,
NSSet/CFSetRef, NSData/CFDataRef, NSNumber/CFNumberRef, NSDate/CFDateRef, NSURL/CFURLRef
and NSError/CFErrorRef. NSRunLoop/CFRunLoopRef and NSBundle/CFBundleRef are not bridged
despite the matching names — check the documentation rather than assuming.
Wrapping an Opaque C Type
The most valuable interop pattern: hide a C or C++ resource inside an Objective-C class so the rest of the codebase never sees it, and so its lifetime is managed by ARC.
// Database.h -- no sqlite3 anywhere in sight
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Database : NSObject
- (nullable instancetype)initWithPath:(NSString *)path error:(NSError **)error;
- (nullable NSArray<NSDictionary<NSString *, id> *> *)query:(NSString *)sql error:(NSError **)error;
- (instancetype)init NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END
// Database.m
#import "Database.h"
#import <sqlite3.h>
@implementation Database {
sqlite3 *_db; // the opaque C handle, private
}
- (nullable instancetype)initWithPath:(NSString *)path error:(NSError **)error {
self = [super init];
if (self) {
int rc = sqlite3_open(path.UTF8String, &_db);
if (rc != SQLITE_OK) {
if (error) {
*error = [NSError errorWithDomain:MyDatabaseErrorDomain
code:rc
userInfo:@{ NSLocalizedDescriptionKey:
@(sqlite3_errmsg(_db)) }];
}
sqlite3_close(_db); // clean up before failing
_db = NULL;
return nil;
}
}
return self;
}
- (void)dealloc {
if (_db) {
sqlite3_close(_db); // the C resource is freed with the object
_db = NULL;
}
}
@end
What this buys: the handle’s lifetime is tied to an ARC-managed object, C errors become NSError`s,
the C header is imported in exactly one file, and callers write ordinary Objective-C. Apply the same shape to
a C++ engine (holding a `std::unique_ptr in a .mm), a file descriptor, or any malloc-ed resource.
Where the resource must be released on a particular thread, or where cleanup can fail, add an explicit
close/invalidate method as well — dealloc runs at an unpredictable moment and must not block.
See Also
-
Automatic Reference Counting — the bridging casts in full.
-
Strings, Numbers and Values —
NSStringand C strings. -
Modules, Frameworks and Code Organization — header visibility and Clang modules.
-
Build and Tooling — the compiler flags involved.