- Logos is great to use, however sometimes there are scenarios when using no Logos (or just Substrate-hooking) is better to use.
- Did you know that after all you're writing "Substrate" tweaks all the time? "But how? I literally use Logos syntax". That's true, but Theos does not. If you have stared at the theos compilation process before for whatever reason while waiting for it to compile, you may have noticed that it starts with
==> Preprocessing Tweak.x…
, this is because Logos is exactly that. A preprocessor written in Perl to make Objective-C hooking feel easier and snappier. But what is Logos actually doing? Well, if you dig into the folder theos generates after the preprocessing is done (.theos/), you'll see that's it's just a wrapper for the MobileSubstrate API, the one Saurik (the creator of Cydia) made a looong time ago to make it possible to create tweaks in a safer way than the original used back then, in the days of yore, (called swizzling). It's very interesting actually, since you're swapping methods' implementations at runtime, basically hijacking the original one and replacing it with your own. More information about this will be linked at the end.
The Theos team made quite an impressive job so that you don't have to write usually "verbose" code and the burden is taken out of you. "But is writing tweaks with the MobileSubstrate API that hard?" No actually, not at all. However, there are some concepts you need to understand first, and as mentioned above, benefits will arise by writing tweaks this way.
- "What are all of these 'benefits' you keep mentioning?"
-
The preprocessing before compiling is no longer needed so it's skipped, thus speeding noticeably the compilation process, specially for larger projects.
-
You'll have native syntax highlighting for .m (Objective-C) files in almost all editors without the need to install additional plugins, since it's a native language, as well as the editor's native vanilla autocompletion.
-
By default, all Logos hooks are initialized in a constructor which you can either create or not, you may be familiarized with this if you ever wrote a
%ctor {}
block. However, with MobileSubstrate, you can control where and when your hooks are initialized. Useful e.g if you need to load your tweak before another tweak and/or process is loaded. -
You can have almost a native xcode like autocompletion if you wanted to. "Really?" Yes, really. Theos has a "secret" command named
make commands
(not yet in upstream) that will automatically generate acompile_commands.json
file which will do all the magic as long as you haveclangd
with LSP (Language Server Protocol) installed, which is what enables autocompletion for a specific language. There's support for C, C++, Objective-C and Swift files. For instructions on how to install this for your editor please go here.
Cheers to Kabir for making this (Theos maintainer and creator of Orion). At the time of writing this (Feb. 11 2022), it still doesn't work for Logos files, hence why this is a major advantage if not the "best", of writing Substrate tweaks.
- Since we will not be using Logos at all, we will rename the
Tweak.xm
file toTweak.m
. TheTweak.xm
in the Makefile also needs to be renamed toTweak.m
so the file can be compiled properly.
-
All of the keywords you see below, preeceded by the
%
symbol are exclusive to Logos, which are all wrappers for different parts of the MobileSubstrate API. -
%hook
- Wrapper forMSHookMessageEx
. -
%new
- Wrapper forclass_addMethod
. -
%property
- Wrapper for a powerful Objective-C 2.0 runtime feature: associated objects. -
%hookf
- a wrapper forMSHookFunction
. (Used to hook C functions). -
%ctor
- wrapper for__attribute__((constructor))
. -
%dtor
- wrapper for__attribute__((destructor))
. -
%init
- It's equivalent in Substrate is a group ofMSHook
calls. -
%subclass
- Usesobjc_allocateClassPair
andobjc_registerClassPair
to create a subclass of a class at runtime. -
%c
- very useful wrapper forobjc_getClass()
, however there's alsoNSClassFromString()
. They are basically the same, but the latter one is more "Objective-C friendly". If you use it.. just make sure to spell the class name correctly. -
%orig
- This does not have an exact equivalent in Substrate. To achieve the effect of%orig
a little bit of Objective-C runtime magic is needed, with the help of the MobileSubstrate API. It'll be explained in the examples below. -
If you want to know more about the constructor and destructor attributes, read through here and here
-
More information about Logos is also available here
Let's say we have some easy tweak to set a view's opacity to 50%, written in Logos.
@import UIKit;
@interface SomeView : UIView
@end
%hook SomeView
- (void)didMoveToSuperview {
self.alpha = 0.5;
%orig;
}
%end
First we need to import the Substrate framework.
@import UIKit;
#import <substrate.h>
@interface SomeView : UIView
@end
%hook SomeView
- (void)didMoveToSuperview {
self.alpha = 0.5;
%orig;
}
%end
Then we will add the wrapper for %ctor
, a.k.a. __attribute__((constructor))
at the bottom.
You have to add that attribute before you create the initializing C function, so that it'll act as an initializer for all the MSHookMessageEx
messages you pass there.
@import UIKit;
#import <substrate.h>
@interface SomeView : UIView
@end
%hook SomeView
- (void)didMoveToSuperview {
self.alpha = 0.5;
%orig;
}
%end
__attribute__((constructor)) static void initialize() {
}
Then we would convert the %hook
wrapper to MSHookMessageEx
.
@import UIKit;
#import <substrate.h>
@interface SomeView : UIView
@end
%hook SomeView
- (void)didMoveToSuperview {
self.alpha = 0.5;
%orig;
}
%end
__attribute__((constructor)) static void initialize() {
MSHookMessageEx(
NSClassFromString(@"SomeView"),
@selector(didMoveToSuperview),
(IMP) &override_didMoveToSuperview,
(IMP *) &orig_didMoveToSuperview
);
}
Finally, we would have to "convert" the Objective-C method we are hooking to the C function that every Objective-C method gets "translated" to.
@import UIKit;
#import <substrate.h>
@interface SomeView : UIView
@end
static void (*orig_didMoveToSuperview)(SomeView *self, SEL _cmd);
static void override_didMoveToSuperview(SomeView *self, SEL _cmd) {
self.alpha = 0.50;
orig_didMoveToSuperview(self, _cmd);
}
__attribute__((constructor)) static void initialize() {
MSHookMessageEx(
NSClassFromString(@"SomeView"),
@selector(didMoveToSuperview),
(IMP) &override_didMoveToSuperview,
(IMP *) &orig_didMoveToSuperview
);
}
- First of all, we create two C functions which take two parameters, a pointer to
self
, so you can use it in the functions just like you would in Logos tweaks to refer to the current object, and aSEL
variable (shorthand for selector) which can be used if you are passing severalMSHookMessageEx
's messages with the same C function for different selectors, so_cmd
can be used to differentiate.
- That one takes four parameters, and the implementation of it, quoting from Saurik's page, looks like this:
void MSHookMessageEx(Class _class, SEL message, IMP hook, IMP *old);
-
A
Class
object which is an Objective-C class in which a message will be passed to, could also be a metaclass if you were to hook a class method, for which you would useobjc_getMetaClass
. -
A
SEL
variable which will point to an Objective-C selector with the message that'll be passed. -
An
IMP
is a typedef for a C function pointer. In other words, the address of a compatible replacement for the implementation of the message being passed. -
An
IMP *
is a pointer to a function pointer that will be filled in with a stub which may be used to call the original implementation. By passing a pointer to it,MSHookMessageEx
will have the ability to set an address for the function pointer so that it points to the original function rather thannullptr
.
This can beNULL
if there are no intentions of calling it. PLEASE notice, you're NOT calling%orig;
here, this is just a variable which may be used if you wish to, but if you don't, it's better to passNULL
to avoid redundancies.
// let's say we're hooking SBDockView's method:
- (void)setBackgroundAlpha:(CGFloat)alpha;
// it would "become"
void setBackgroundAlpha(SBDockView *self, SEL _cmd, CGFloat alpha);
This is definitely not an all-inclusive overview of Substrate-hooking, but it is a great starting place.
If you're wondering what or why are we using the static
keyword before the function name, it allows for additional encapsulation, restricting the functions to this file only, which is good programming practice. Note that it can also mean something else for variables and it's not necessarily the same as the static keyword in Swift
More information about it can be found here and here, as well as here for method swizzling if you're curious.
Previous Page (Preference Bundles cont.)
- File co-authored with Luki120