A native iOS screen in C+
This is a real iOS app: a UIWindow with a white background and a centered
label, with the UI written in C+ through the uikit
bindings. It cross-compiles with one flag, Xcode links it like any static
library, and it renders on the simulator. The only C in the project is a
two-line main shim.
uikit mirrors appkit: real UIView objects over
ObjC-runtime FFI, closure-free callbacks, and owned wrappers that release in
drop. iOS is a cross-compile target, so cpc stops at the object and Xcode
owns the final link — see Targets & cross-compilation.
How an iOS app starts
UIApplicationMain never returns, so the flow is: the C+ app exports
cplus_app_main, which hands off to application::run with a
didFinishLaunchingWithOptions: implementation. That callback builds the window
and views and returns 1.
Full source
src/main.cplus — the whole app:
// A native iOS screen, with the UI in C+. Cross-compiled with
// `cpc build --target ios-arm64-simulator`; Xcode links it and owns main.
import "uikit/application" as application;
import "uikit/window" as window;
import "uikit/controllers" as controllers;
import "uikit/view" as view;
import "uikit/screen" as screen;
import "uikit/runtime" as rt;
// `application:didFinishLaunchingWithOptions:`. Build the UI here, return 1.
fn did_finish(_self: *u8, _cmd: *u8, _app: *u8, _options: *u8) -> i8 {
let bounds: rt::Rect = screen::Screen::main().bounds();
// Root view controller with a white view.
let vc: controllers::ViewController = controllers::ViewController::new();
vc.view().set_background_color(view::Color::white());
// A centered label, half-way down the screen.
let label: view::Label = view::Label::new(
rt::make_rect(0.0, bounds.size.height / 2.0 - 20.0, bounds.size.width, 40.0));
label.set_text("Hello from C+");
label.set_text_alignment(1 as i64); // NSTextAlignmentCenter
label.set_text_color(view::Color::black());
vc.view().add_subview(label.obj);
// The key window owns the controller; UIKit keeps it for the process.
let w: window::Window = window::Window::new(bounds);
w.set_root_view_controller(vc);
w.make_key_and_visible();
return 1;
}
// Exported entry point. Xcode's main.c shim calls this (see Reproduce).
pub extern fn cplus_app_main(argc: i32, argv: *u8) -> i32 {
return application::run(argc, argv, did_finish);
}
There are no closures: did_finish is a plain named function handed to
application::run, exactly the way UIKit wants an app-delegate method. Widgets
are real UIViews; label.obj is the underlying id handed to
add_subview.
Results
Built with cpc build --target ios-arm64-simulator and linked by Xcode.
| Build output | an iOS staticlib (object + archive + C header) in target/ios-arm64-simulator/debug/ |
| Final link | Xcode (the iOS targets stop at object emission) |
| On the simulator | a white screen with a centered "Hello from C+", on the iPhone 16 Pro simulator |
| C in the project | a two-line main shim — everything else is C+ |
The point: the screen you see is driven by C+ calling UIKit directly, with no binding-generator and no glue layer, the same model appkit uses on the desktop.
Reproduce
You need cpc 0.0.21 or newer and Xcode (for the iOS SDK and the final link).
1. The C+ static library. Put the source above in src/main.cplus with a
manifest that builds a staticlib and depends on uikit:
[package]
name = "hello_ios"
version = "0.0.1"
edition = "2026"
[lib]
name = "app"
[dependencies]
uikit = "*"
cpc build --target ios-arm64-simulator # or ios-arm64 for a device
cpc emits the object, archive (libapp.a), and a C header; it never runs the
link.
2. The two-line shim. In the Xcode app target, a single C file enters the C+ app:
extern int cplus_app_main(int argc, char **argv);
int main(int argc, char **argv) { return cplus_app_main(argc, (void *)argv); }
3. Link in Xcode. Add libapp.a to the target's Link Binary With
Libraries, along with UIKit, Foundation, and libobjc (the
frameworks the [link] table names belong on Xcode's link line). Build and run
on the simulator; the C+-driven screen appears.
‹ Back to all examples