Objective-C 연동
Objective-C 연동 (Interfacing to Objective-C)
D 언어는 Objective-C와 연동할 수 있어요. 프로토콜(protocol), 클래스, 서브클래스, 인스턴스 변수, 인스턴스 메서드, 클래스 메서드를 모두 지원한답니다. 다만 플랫폼별 지원 범위는 컴파일러에 따라 조금씩 다를 수 있다는 점은 참고해 두세요.
본문
완전히 동작하는 예제는 맨 아래에서 확인할 수 있어요.
Classes (클래스)
Declaring an External Class (외부 클래스 선언)
import core.attribute : selector;
extern (Objective-C)
extern class NSString
{
const(char)* UTF8String() @selector("UTF8String");
}
D 안에서 접근해야 하는 모든 Objective-C 클래스는 Objective-C linkage로 선언해야 해요. extern (Objective-C)에 더해 클래스를 extern으로 선언하면, 그 클래스는 외부에서 정의되어 있다고 간주됩니다.
@selector 속성은 이 메서드를 호출할 때 어떤 Objective-C 셀렉터를 사용할지를 알려주는 역할을 해요. 이 속성은 Objective-C linkage를 가진 모든 메서드에 붙어야 합니다.
Binding to a @property (Accessor Methods) (@property 바인딩)
import core.attribute : selector;
extern (Objective-C)
extern class MTLRenderPipelineDescriptor : NSObject
{
NSString label() @selector("label");
NSString label(NSString) @selector("setLabel:");
}
Objective-C 클래스의 @property를 바인딩할 때는, 프로퍼티가 getter와 setter를 함께 만들어 낸다는 사실을 알고 있어야 해요. 값을 읽어오는 메서드(getter)는 프로퍼티와 같은 이름을 쓰고, 값을 설정하는 메서드(setter)는 "set"이라는 단어로 시작해서 프로퍼티 이름을 대문자로 이어 붙인 형태를 가집니다. 예를 들어 label 프로퍼티의 setter는 setLabel:이 되는 거죠.
Defining a Class (클래스 정의)
import core.attribute : selector;
// externally defined
extern (Objective-C)
extern class NSObject
{
static NSObject alloc() @selector("alloc");
NSObject init() @selector("init");
}
extern (Objective-C)
class Foo : NSObject
{
override static Foo alloc() @selector("alloc");
override Foo init() @selector("init");
final int bar(int a) @selector("bar:")
{
return a;
}
}
void main()
{
assert(Foo.alloc.init.bar(3) == 3);
}
Objective-C 클래스를 정의하는 방법은 외부 클래스를 선언하는 것과 똑같아요. 다만 이 경우에는 extern으로 선언하면 안 된다는 점만 다릅니다.
Objective-C의 의미(semantics)를 맞추기 위해, static과 final 메서드는 가상(virtual) 메서드로 취급돼요. static 메서드도 오버라이드가 가능하답니다.
Protocols (프로토콜)
Declaring a Protocol (프로토콜 선언)
import core.attribute : selector;
import core.stdc.stdio : printf;
extern (Objective-C)
interface Foo
{
static void foo() @selector("foo");
void bar() @selector("bar");
}
extern (Objective-C)
class Bar : Foo
{
static void foo() @selector("foo")
{
printf("foo\n");
}
void bar() @selector("bar")
{
printf("bar\n");
}
}
Objective-C 프로토콜은 D에서는 인터페이스로 표현되며, interface 키워드로 선언해요.
D 안에서 접근해야 하는 모든 Objective-C 프로토콜도 Objective-C linkage로 선언해야 합니다.
Objective-C 프로토콜은 클래스 가상(static) 메서드를 지원해요. 이런 메서드는 프로토콜을 구현하는 클래스가 반드시 구현해야 하죠 (단, optional 메서드라면 예외입니다). 이런 의미를 맞추기 위해 static 메서드도 가상으로 취급됩니다. 그래서 인터페이스 안의 Objective-C linkage static 메서드는 본문(body)을 가질 수 없어요.
Optional Methods (옵셔널 메서드)
import core.attribute : optional, selector;
import core.stdc.stdio : printf;
struct objc_selector;
alias SEL = objc_selector*;
extern (C) SEL sel_registerName(in char* str);
extern (Objective-C)
extern class NSObject
{
static NSObject alloc() @selector("alloc");
NSObject init() @selector("init");
}
extern (Objective-C)
interface Foo
{
bool respondsToSelector(SEL sel) @selector("respondsToSelector:");
void foo() @selector("foo");
// this is an optional method
@optional void bar() @selector("bar");
}
extern (Objective-C)
class Bar : NSObject, Foo
{
override static Bar alloc() @selector("alloc");
override Bar init() @selector("init");
bool respondsToSelector(SEL sel) @selector("respondsToSelector:");
void foo() @selector("foo")
{
printf("foo\n");
}
}
void main()
{
Foo f = Bar.alloc.init;
// check, at runtime, if the instance `f` implements the method `bar`
if (f.respondsToSelector(sel_registerName("bar")))
f.bar();
else
f.foo();
}
Objective-C 프로토콜은 옵셔널 메서드(optional method)를 지원해요. 옵셔널 메서드는 프로토콜을 구현하는 클래스가 꼭 구현할 필요는 없습니다. 옵셔널 메서드 호출은 안전하게 하기 위해, 런타임에서 수신자가 그 메서드를 실제로 구현하는지 확인하는 검사를 거치는 게 좋아요.
D에서는 옵셔널 메서드를 @optional 속성으로 표현합니다.
Instance Variables (인스턴스 변수)
import core.attribute : selector;
// externally defined
extern (Objective-C)
extern class NSObject
{
static NSObject alloc() @selector("alloc");
NSObject init() @selector("init");
}
extern (Objective-C)
class Foo : NSObject
{
int bar_;
override static Foo alloc() @selector("alloc");
override Foo init() @selector("init");
int bar() @selector("bar")
{
return bar_;
}
}
void main()
{
auto foo = Foo.alloc.init;
foo.bar_ = 3;
assert(foo.bar == 3);
}
인스턴스 변수를 선언하는 모습은 일반적인 D 클래스와 완전히 똑같아요.
Objective-C의 인스턴스 변수는 '취약한 기반 클래스 문제(fragile base class problem)'를 해결하기 위해 동적 오프셋(dynamic offset)을 가집니다. 이 말은, 기반 클래스가 인스턴스 변수를 추가하거나 제거해도, 서브클래스가 다시 컴파일하거나 재링크할 필요가 없다는 뜻이에요. 이런 덕분에 Objective-C 클래스에 바인딩을 만들 때 인스턴스 변수를 굳이 선언하지 않아도 됩니다.
Calling an Instance Method (인스턴스 메서드 호출)
Objective-C 인스턴스 메서드를 호출하는 문법은 일반적인 D 메서드를 호출하는 것과 같아요:
const(char)* result = object.UTF8String();
컴파일러는 Objective-C linkage를 가진 메서드에 대한 호출을 보게 되면, Objective-C 컴파일러가 그 메서드를 호출하는 것과 비슷한 코드를 만들어 냅니다.
The @selector Attribute (@selector 속성)
@selector 속성은 컴파일러가 인식하는 UDA예요. Objective-C 메서드를 호출할 때 어떤 셀렉터를 쓸지 컴파일러에게 알려주는 역할을 합니다.
Objective-C의 셀렉터에는 콜론(:) 문자가 들어갈 수 있는데, 이 문자는 D 식별자에서는 쓸 수 없어요. 그리고 D는 메서드 오버로딩을 지원하는 반면, Objective-C는 서로 다른 셀렉터를 써서 비슷한 일을 처리하고요. 이런 두 가지 이유 때문에 D에서는 셀렉터를 추론하기보다 직접 지정하는 편이 낫습니다. 이렇게 하면 D에서 더 자연스러운 메서드 이름을 쓸 수 있어요. 예를 들어 보죠:
import core.attribute : selector;
extern (Objective-C)
extern class NSString
{
NSString initWith(in char*) @selector("initWithUTF8String:");
NSString initWith(NSString) @selector("initWithString:");
}
여기서 initWith 메서드는 두 버전으로 오버로딩돼요. 하나는 in char*를 받고, 다른 하나는 NSString을 받습니다. 이 두 메서드는 initWithUTF8String:과 initWithString:이라는 서로 다른 Objective-C 셀렉터로 매핑됩니다.
이 속성은 druntime의 core.attribute에 정의되어 있어요. 그리고 버전 식별자 D_ObjectiveC가 활성화되었을 때만 정의됩니다.
Compiler Checks (컴파일러 검사)
컴파일러는 @selector 속성의 올바른 사용을 강제하기 위해 다음 검사를 수행합니다:
- 이 속성은 Objective-C linkage를 가진 메서드에만 붙일 수 있다
- 이 속성은 한 메서드에 한 번만 붙일 수 있다
- 이 속성은 템플릿 메서드에는 붙일 수 없다
- 셀렉터 안의 콜론 개수는 메서드가 선언한 매개변수의 개수와 일치해야 한다
이 검사 중 하나라도 실패하면 컴파일 오류가 발생합니다.
The @optional Attribute (@optional 속성)
@optional 속성도 컴파일러가 인식하는 UDA예요. 인터페이스 안에 선언된 Objective-C linkage 메서드가 옵셔널이라는 것을 컴파일러에게 알려주는 역할을 합니다. 즉, 그 인터페이스를 구현하는 클래스가 그 메서드를 꼭 구현할 필요는 없다는 뜻입니다.
옵셔널 메서드를 안전하게 호출하려면, 런타임에서 수신자가 그 메서드를 실제로 구현하는지 확인하는 검사를 거치는 것이 좋아요.
이 속성은 druntime의 core.attribute에 정의되어 있어요. 그리고 버전 식별자 D_ObjectiveC가 활성화되었을 때만 정의됩니다.
Compiler Checks (컴파일러 검사)
컴파일러는 @optional 속성의 올바른 사용을 강제하기 위해 다음 검사를 수행합니다:
- 이 속성은 Objective-C linkage를 가진 메서드에만 붙일 수 있다
- 이 속성은 인터페이스 안의 메서드에만 붙일 수 있다
- 이 속성은 한 메서드에 한 번만 붙일 수 있다
- 이 속성은 템플릿 메서드에는 붙일 수 없다
이 검사 중 하나라도 실패하면 컴파일 오류가 발생합니다.
The D_ObjectiveC Version Identifier (D_ObjectiveC 버전 식별자)
D_ObjectiveC 버전 식별자는 미리 정의된 버전 식별자예요. 대상 플랫폼에서 Objective-C 지원이 가능할 때 활성화됩니다.
Objective-C Linkage (Objective-C linkage)
Objective-C linkage는 클래스에 extern (Objective-C) 속성을 붙여서 만들 수 있어요. 예를 들어:
import core.attribute : selector;
extern (Objective-C)
extern class NSObject
{
NSObject init() @selector("init");
}
extern (Objective-C)로 선언된 클래스 안의 모든 메서드는 자동으로 Objective-C linkage를 가지게 됩니다.
이 linkage는 모든 플랫폼에서 인식되지만, Objective-C 지원이 없는 플랫폼에서 쓰면 컴파일 오류가 발생해요. 덕분에 version 문만으로 Objective-C를 지원하지 않는 플랫폼에서 선언을 쉽게 숨길 수 있습니다. 문자열 믹스인이나 다른 우회 방법을 쓸 필요가 없죠.
Memory Management (메모리 관리)
Objective-C에서 메모리를 관리하는 권장 방법은 자동 참조 카운팅, 즉 ARC를 쓰는 거예요. 그런데 D는 ARC를 지원하지 않으므로, 대신 수동 메모리 관리를 해야 합니다. Objective-C 인스턴스에 release를 호출하는 방식으로요. 마치 Objective-C의 예전 방식처럼 말이죠.
Frameworks (프레임워크)
대부분의 Objective-C 코드는 "Framework(프레임워크)"라는 형태로 묶여 있어요. 이건 기본적으로 .framework 확장자와 특정 디렉터리 구조를 가진 일반 디렉터리랍니다. 프레임워크 안에는 동적 라이브러리, 모든 공개 헤더 파일, 그리고 프레임워크가 필요로 하는 각종 리소스(이미지, 사운드 등)가 들어 있습니다.
이런 디렉터리는 Objective-C 컴파일러나 링커 같은 도구들에게 프레임워크로 인식됩니다. DMD에서 프레임워크와 링크하려면 다음 플래그를 쓰세요:
-L-framework -L<Framework>
Framework Paths (프레임워크 경로)
위 플래그를 쓰면 링커가 표준 프레임워크 경로에서 검색해요. 프레임워크를 찾는 표준 검색 경로는 다음과 같습니다:
/System/Library/Frameworks/Library/Frameworks
DMD의 다음 플래그를 쓰면 프레임워크를 검색할 새 경로를 추가할 수 있어요:
-L-F<framework_path>
더 자세한 내용은 참고 문서와 ld 매뉴얼 페이지를 참고해 주세요.
Full Usage Example (전체 사용 예제)
이 예제는 Objective-C 문자열 NSString을 만들고, NSLog를 이용해 메시지를 stderr로 기록합니다.
import core.attribute : selector;
extern (Objective-C)
extern class NSString
{
static NSString alloc() @selector("alloc");
NSString initWithUTF8String(in char* str) @selector("initWithUTF8String:");
void release() @selector("release");
}
이건 NSString 클래스의 단순화된 선언이에요. alloc 메서드는 클래스의 인스턴스를 할당하죠. initWithUTF8String: 메서드는 UTF-8 C 문자열을 Objective-C 문자열 NSString로 변환하는 데 쓰입니다. release 메서드는 문자열을 해제하고 할당을 되돌리는 용도예요. D는 ARC를 지원하지 않으므로, Objective-C 인스턴스를 수동으로 release 해줘야 합니다.
extern (C) void NSLog(NSString, ...);
이 NSLog 함수는 메시지를 System Log 시설로 출력해요. 즉 stderr와 Console에 메시지를 띄운다는 뜻입니다.
auto str = NSString.alloc();
클래스 NSString의 인스턴스를 하나 할당합니다.
str = str.initWithUTF8String("Hello World!")
C 문자열을 사용해 Objective-C 문자열을 초기화해요.
NSLog(str);
문자열을 stderr로 기록하면 터미널에 대략 이런 내용이 찍힙니다:
2015-07-18 13:14:27.978 main[11045:2934950] Hello World!
str.release();
문자열을 해제하고 할당을 되돌립니다.
모든 단계를 합치면 이렇게 됩니다:
module main;
import core.attribute : selector;
extern (Objective-C)
extern class NSString
{
static NSString alloc() @selector("alloc");
NSString initWithUTF8String(in char* str) @selector("initWithUTF8String:");
void release() @selector("release");
}
extern (C) void NSLog(NSString, ...);
void main()
{
auto str = NSString.alloc().initWithUTF8String("Hello World!");
NSLog(str);
str.release();
}
애플리케이션을 컴파일할 때는 필요한 라이브러리와 함께 링크하는 걸 잊지 마세요. 여기서는 Foundation 프레임워크가 필요하죠. 예를 들면:
dmd -L-framework -LFoundation main.d
더 알아보기 (Learn more)
- Objective-C 셀렉터와
@selector속성,@optional속성의 컴파일러 검사 규칙 extern (Objective-C)linkage와D_ObjectiveC버전 식별자- 프레임워크 링크 플래그(
-L-framework,-L-F) - 관련 챕터: Interfacing to C++, Portability Guide