Objective-C 런타임
Objective-C는 동적 프로그래밍 언어로 Clang이나 GCC 같은 컴파일러에 의해 컴파일되어 실행되지만, 런타임 시스템은 동적으로 작동한다. 이것이 C언어 위에 동적인 객체 시스템을 구성하여 Foundation, Cocoa 같은 프레임웍을 만들게 된다.
이 런타임 시스템에 낮은 수준에서 접근하여 유용한 일을 할 수 있다. 이어질 다음 글에서 Cocoa에 사용되는 패턴 중 하나를 살펴보겠지만, 그 외에도 Foundation, Cocoa 등의 프레임웍이나 Objective-C와의 자동화된 스크립트 언어 바인딩을 구현하거나, 이 기반들과 호환되는 새로운 프로그래밍 언어를 만들 수도 있다. (Swift)
그중에서도 이 글에서는 런타임에 새로운 클래스를 선언하고, 그 클래스의 인스턴스를 생성하여 멤버 메소드를 호출해보는 것으로 Objective-C 런타임의 사용법을 간략히 소개한다. 프로토콜
등을 다루는 것은 새로운 프로그래밍 언어를 구현하지 않는 이상 이 글의 의도를 넘어선다. Objective-C 런타임 라이브러리의 구체적인 사용법에 대해서는 레퍼런스에서 볼 수 있다.1
이 글에서는 다음의 Objective-C 클래스 구현을 런타임에 하려 한다.
@interface Customer : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Customer
@end
실제로 컴파일된 결과는 위의 코드에서 자동으로 많은 부분이 생성synthesize된 것인데, 실제 모습은 (간략히) 다음과 같다.
@interface Customer : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Customer {
NSString *_name;
}
- (NSString *)name {
return _name;
}
- (void)setName:(NSString *)name {
_name = [name copy];
}
@end
위 선언과 구현이 더 이상 없는 것으로 간주하고 런타임으로 코드를 옮겨본다. 편의상 Objective-C 코드도 사용되고 있지만, 이 글을 읽고 나면 스스로 완전한 C 코드로 바꿀 수 있다. 먼저 Customer
라는 Class 객체를 선언하자. (편의상 전역 스코프를 가진다) 그리고 name
프로퍼티에 접근할 getter와 setter를 C 함수로 구현한다.
#include <objc/objc-runtime.h>
Class Customer;
NSString *name(id self, SEL _cmd) {
Ivar ivar = class_getInstanceVariable(Customer, "_name");
return object_getIvar(self, ivar);
}
void setName(id self, SEL _cmd, NSString *newName) {
Ivar ivar = class_getInstanceVariable(Customer, "_name");
id oldName = object_getIvar(self, ivar);
if (oldName != newName) {
object_setIvar(self, ivar, [newName copy]);
}
}
실제로 Objective-C의 메소드 구현은 C 함수의 프로토타입(IMP라 한다)을 가진다. Objective-C의 메소드 호출은 메시지
를 받을 객체에게 전달하는 것이지만, 메시지를 받은 객체는 메소드에 해당하는 실제 C 함수를 호출한다. 인스턴스 메소드 IMP 함수의 프로토타입은 Objective-C 메소드의 리턴형과 호출된 객체의 self
, 호출된 메소드의 셀렉터 _cmd
, 이후에 실제 메소드 인자들 순으로 나열된다. self
와 _cmd
는 Objective-C 메소드에서도 직접 참조가 가능한 값이다.2 아래에서 추가될 프로퍼티를 위한 인스턴스 변수3 _name
을 어떻게 참조하는지 볼 수 있다. 이제 실제로 클래스를 정의한다.
Customer = objc_allocateClassPair([NSObject class], "Customer", 0);
NSObject 클래스를 부모 클래스로 하는 Customer
라는 클래스를 생성한다.
class_addIvar(Customer, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
Customer
클래스에 _name
인스턴스 변수를 추가한다. 마지막 인자에 런타임 시스템이 사용하는 실제 타입 정보를 제공한다. @encode
는 Objective-C의 타입을 런타임 시스템이 사용하는 내부적인 타입으로 변환하는 컴파일러 지시자
이다.4 런타임에 필요한 실제적인 타입 정보를 필요로 하는 곳은 모두 이 인코딩을 사용한다.
objc_property_attribute_t type = { "T", "@\"NSString\"" };
objc_property_attribute_t backingIvar = { "V", "_name" };
objc_property_attribute_t attrs[] = { type, backingIvar };
class_addProperty(Customer, "name", attrs, sizeof(attrs) / sizeof(attrs[0]));
위에서 추가한 _name
인스턴스 변수를 사용하는 name
프로퍼티를 추가한다.
class_addMethod(Customer, @selector(name), (IMP)name, "@@:");
class_addMethod(Customer, @selector(setName:), (IMP)setName, "v@:@");
name
프로퍼티의 getter와 setter 메소드를 추가한다. 객체 외부로 노출될 셀렉터와 내부 구현부인 IMP 함수가 지정된 것을 볼 수 있다. 마지막 인자는 위에서도 언급한 타입 정보로, @
는 id
타입, v
는 void
타입, :
는 셀렉터 SEL
타입 등이다. 첫 번째는 리턴형이므로 두 번째와 세 번째는 모두 @:
이다. self
와 셀렉터 _cmd
가 전달되기 때문이다.
객체가 모두 @
인 것은5 런타임에서 모든 객체는 id
타입으로 메시지를 주고받기 때문이다. 이것이 Objective-C를 동적 언어로 만드는 중요한 부분이다.
objc_registerClassPair(Customer);
objc_registerClassPair
함수를 통해 클래스를 Objective-C 런타임에 등록한다. 이제 Customer
객체를 아래와 같이 생성할 수 있다.
id customer = class_createInstance(Customer, 0);
SEL getter = @selector(name);
SEL setter = @selector(setName:);
((void (*)(id, SEL, id))objc_msgSend)(customer, setter, @"John Appleseed");
NSString *name = ((id (*)(id, SEL))objc_msgSend)(customer, getter);
objc_msgSend
함수의 경우 AArch64 아키텍처가 도입되면서 프로토타입이 void
타입으로 변경되었다. 실제 호출될 IMP 함수의 프로토타입에 맞춰 캐스팅이 필요하다. Objective-C 문법으로 옮기면 아래와 같다.
id customer = [[Customer alloc] init];
[customer setName:@"John Appleseed"];
NSString *name = [customer name];
id
타입의 객체로 보내는 메시지에 대해서는 타입 검사를 하지 않기 때문에 앞서 메소드가 선언되지 않았더라도 컴파일러가 에러를 일으키지 않는다. 대신 프로퍼티 접근자를 사용하여 customer.name
과 같이 접근할 수는 없다.
이런 방법이 실제 Cocoa 프레임웍에서 어떻게 활용되고 있는지 이어지는 글에서 살펴볼 예정이다.
Objective-C Runtime (Apple Developer Documentation) ↩︎
따라서, 실제 Objective-C 메소드 구현과 동일한 코드를 IMP 함수에서 사용할 수 있다. ↩︎
backing instance variable이라 부른다. ↩︎
Type Encodings (Objective-C Runtime Programming Guide) ↩︎
실제로
id
를 포함하여 NSObject 등의 객체 타입을@encode
하면@
값이 나온다. ↩︎