How tweaks work
Cyanide tweaks are app-side drivers. No SpringBoard dylibs, no Substrate hooks, no swizzled methods. The app reaches into the target from outside.
A RemoteCall session is the bridge. From inside one you send Objective-C messages, read and write memory, and call C symbols in the target process.
Settings holds the SpringBoard channel during Apply Tweaks. Your code runs inside it under settings_rc_lock(), via three entrypoints: apply_in_session, optional stop_in_session, and forget_remote_state.
The remote_objc API
Import remote_objc.h and ../TaskRop/RemoteCall.h. Helpers assume an active session — don’t call init_remote_call yourself unless you need a private channel.
r_class("UILabel") // remote Class *
r_sel("setHidden:") // remote SEL
r_msg2(obj, "setHidden:", 1,0,0,0) // objc_msgSend in target
r_msg2_main(label, "setText:", text,
0,0,0) // UIKit/main-thread send
r_msg2_main_raw(obj, "setFrame:",
&rect, sizeof(rect), NULL,0,
NULL,0, NULL,0) // pass a struct by value
r_msg2_main_struct_ret(obj, "bounds",
&out, sizeof(out), NULL,0,
NULL,0, NULL,0, NULL,0) // copy a struct return
r_alloc_str("hi") / r_free(ptr) // C string into remote
r_nsstr_retained("hi") // NSString*, caller releases
r_cfstr("hi") // CFStringRef, caller CFReleases
r_settle_us(1000) // tune helper delay; restore old value
r_dlsym_call(R_TIMEOUT,
"objc_setAssociatedObject",
obj, key, val, policy, 0,0,0,0) // any C function
r_is_objc_ptr(p) // sanity check
r_ivar_value(obj, "_name") // read ivar
r_responds_main(obj, "sel:") // -respondsToSelector:
remote_read / remote_write // raw memory helpers
init_remote_call("SpringBoard", false)
destroy_remote_call() // one-shot sessions only
abandon_remote_call() // remote task is already goneA minimal tweak
A complete RemoteCall-only tweak: paints an 80×80 red square on a SpringBoard window.
Idempotent on reapply, undoes itself on stop, drops cached pointers on respawn.
#ifndef hello_tweak_h
#define hello_tweak_h
#include <stdbool.h>
bool hello_tweak_apply_in_session(void);
bool hello_tweak_stop_in_session(void);
void hello_tweak_forget_remote_state(void);
#endif#import "hello_tweak.h"
#import "remote_objc.h"
#import "../TaskRop/RemoteCall.h"
#import <stdint.h>
static const uint64_t kHelloTag = 0xC0A11DE;
static uint64_t gHelloView = 0;
static uint64_t hello_first_window(void) {
uint64_t UIApplication = r_class("UIApplication");
uint64_t app = r_msg2_main(UIApplication, "sharedApplication",
0, 0, 0, 0);
if (!r_is_objc_ptr(app)) return 0;
uint64_t keyWindow = r_msg2_main(app, "keyWindow", 0, 0, 0, 0);
if (r_is_objc_ptr(keyWindow)) return keyWindow;
uint64_t windows = r_msg2_main(app, "windows", 0, 0, 0, 0);
uint64_t count = r_msg2_main(windows, "count", 0, 0, 0, 0);
for (uint64_t i = 0; r_is_objc_ptr(windows) && i < count && i < 16; i++) {
uint64_t window = r_msg2_main(windows, "objectAtIndex:", i, 0, 0, 0);
if (r_is_objc_ptr(window)) return window;
}
return 0;
}
static uint64_t hello_existing_view(uint64_t window) {
if (!r_is_objc_ptr(window)) return 0;
uint64_t view = r_msg2_main(window, "viewWithTag:", kHelloTag, 0, 0, 0);
if (r_is_objc_ptr(view)) gHelloView = view;
return r_is_objc_ptr(view) ? view : 0;
}
bool hello_tweak_apply_in_session(void) {
uint64_t window = hello_first_window();
if (!r_is_objc_ptr(window)) return false;
uint64_t existing = hello_existing_view(window);
if (r_is_objc_ptr(existing)) {
r_msg2_main(existing, "setHidden:", 0, 0, 0, 0);
r_msg2_main(window, "bringSubviewToFront:", existing, 0, 0, 0);
return true;
}
uint64_t UIView = r_class("UIView");
uint64_t view = r_msg2_main(r_msg2_main(UIView, "alloc", 0, 0, 0, 0),
"init", 0, 0, 0, 0);
if (!r_is_objc_ptr(view)) return false;
struct { double x, y, w, h; } frame = { 40.0, 120.0, 80.0, 80.0 };
r_msg2_main_raw(view, "setFrame:",
&frame, sizeof(frame),
NULL, 0, NULL, 0, NULL, 0);
uint64_t UIColor = r_class("UIColor");
uint64_t color = r_msg2_main(UIColor, "systemRedColor", 0, 0, 0, 0);
if (!r_is_objc_ptr(color)) color = r_msg2_main(UIColor, "redColor", 0, 0, 0, 0);
r_msg2_main(view, "setBackgroundColor:", color, 0, 0, 0);
r_msg2_main(view, "setTag:", kHelloTag, 0, 0, 0);
r_msg2_main(window, "addSubview:", view, 0, 0, 0);
r_msg2_main(view, "release", 0, 0, 0, 0);
gHelloView = view;
return true;
}
bool hello_tweak_stop_in_session(void) {
uint64_t window = hello_first_window();
uint64_t view = hello_existing_view(window);
if (!r_is_objc_ptr(view)) return false;
r_msg2_main(view, "setHidden:", 1, 0, 0, 0);
r_msg2_main(view, "removeFromSuperview", 0, 0, 0, 0);
gHelloView = 0;
return true;
}
void hello_tweak_forget_remote_state(void) {
// SpringBoard respawned or RemoteCall was abandoned; cached
// remote pointers are from the old address space.
gHelloView = 0;
}Wiring into Settings
SettingsViewController.m is the orchestrator. Add five things: a defaults key, a switch row, a Run-path apply, a live-apply branch, and forget_remote_state in cleanup.
Every apply checks g_springboard_rc_ready inside @synchronized(settings_rc_lock()). settings_mark_tweak_applied() keeps package state honest. forget_remote_state() runs on respring and abandon.
#import "tweaks/hello_tweak.h"
NSString * const kSettingsHelloEnabled = @"HelloEnabled";
// Add kSettingsHelloEnabled to settings_register_defaults(),
// settings_rc_backed_tweak_keys(), settings_key_affects_package_state(),
// and the Settings rows that render the switch.
static BOOL settings_key_is_hello(NSString *key) {
return [key isEqualToString:kSettingsHelloEnabled];
}
// In the Run path, after settings_ensure_springboard_remote_call_locked():
if ([d boolForKey:kSettingsHelloEnabled]) {
bool ok = hello_tweak_apply_in_session();
settings_mark_tweak_applied(kSettingsHelloEnabled,
ok && [d boolForKey:kSettingsHelloEnabled]);
printf("[SETTINGS] Hello result=%d\n", ok);
}
// In settings_schedule_live_apply_for_key():
if (settings_key_is_hello(key)) {
if ([d boolForKey:kSettingsHelloEnabled] && g_springboard_rc_ready) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (settings_rc_lock()) {
if (settings_cleanup_in_progress() || !g_springboard_rc_ready) return;
bool ok = hello_tweak_apply_in_session();
settings_mark_tweak_applied(kSettingsHelloEnabled,
ok && [d boolForKey:kSettingsHelloEnabled]);
}
settings_notify_package_queue_changed_async();
});
} else if (![d boolForKey:kSettingsHelloEnabled]) {
settings_mark_tweak_applied(kSettingsHelloEnabled, NO);
settings_notify_package_queue_changed_async();
if (g_springboard_rc_ready) dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (settings_rc_lock()) {
if (g_springboard_rc_ready) hello_tweak_stop_in_session();
}
});
}
return;
}
// In SpringBoard restart/abandon and manual cleanup paths:
hello_tweak_forget_remote_state();Porting from Theos / Substrate
RemoteCall isn’t a hook framework. You can’t intercept a method or replace a C function in place.
Ports work when the effect is a finite mutation — set this property, call this controller method, add this view, hold this assertion, or refresh on a timer.
%hook UIView not portable as a hook
- (void)setHidden:(BOOL)h { ... } rewrite as explicit
r_msg2_main(view,
"setHidden:", h,0,0,0)
[%c(Foo) bar] r_msg2(r_class("Foo"),
"bar", 0,0,0,0)
struct { double x,y,w,h; } r = {0}; r_msg2_main_struct_ret(view,
"bounds", &r, sizeof(r),
NULL,0, NULL,0, NULL,0, NULL,0)
%new -[X cyanideOverlay] associated object via
objc_setAssociatedObject
through r_dlsym_call
MSHookFunction(...) not available hereTargeting another process? Open a separate session with init_remote_call(name, false), do the work, destroy_remote_call before switching back. Powercuff does this for thermalmonitord.
Contribute
Build with ./scripts/build.sh — the IPA is packaged under build/. Sideload, test on device, attach Log-tab output to your PR.
Source and issues: github.com/zeroxjf/cyanide-ios