NativeCall

NativeCall

Raku 프로그램에서 네이티브(C) 라이브러리의 함수를 부르고 싶을 때가 있어요. NativeCall은 바로 그 일을 도와주는 핵심 모듈이에요. 평범한 Raku 서브루틴 선언에 is native 트레잇 하나만 붙이면, 그 함수가 네이티브 라이브러리에 실제로 정의돼 있다는 사실을 Raku가 알게 돼요. 이 문서에서는 NativeCall의 기본 사용법부터 함수 인자 전달, 포인터, 구조체, 배열, 문자열 관리, 그리고 C++ 지원까지 차근차근 살펴볼게요.

출처: Raku Documentation — NativeCall

본문

시작하기 (Getting started)

NativeCall의 가장 단순한 사용법은 이렇게 생겼어요:

use NativeCall;
sub some_argless_function() is native('something') { * }
some_argless_function();

첫 줄은 여러 트레잇과 타입을 가져와요. 다음 줄은 상당히 평범한 Raku 서브루틴 선언처럼 보이지만, 여기에 트위스트가 있어요. native 트레잇을 써서 이 서브루틴이 실제로 네이티브 라이브러리에 정의돼 있음을 지정해요. 플랫폼별 확장자(예: .so.dll)와 관례적인 접두사(예: lib)는 자동으로 붙어요.

"some_argless_function"을 처음 호출하면 "libsomething"이 로드되고 그 안에서 "some_argless_function"을 찾아요. 그 다음 호출이 이루어져요. 이후의 호출은 심볼 핸들이 유지되므로 더 빨라져요.

물론 대부분의 함수는 인자를 받거나 값을 반환해요. 하지만 여러분이 할 수 있는 다른 모든 일은, Raku 서브루틴을 선언하고, 호출하려는 심볼의 이름을 붙이고, native 트레잇으로 표시하는 이 간단한 패턴에 덧붙이는 것뿐이에요.

또한 네이티브 타입도 선언하고 사용해야 하는데, 네이티브 타입은 공유 라이브러리에서 같은 방식으로 가져올 수는 없어요. 네이티브 타입 페이지를 참고하세요.

여러분이 직접 컴파일한 라이브러리나 번들된 라이브러리를 쓰는 경우가 아니라면, 공유 라이브러리는 버전이 붙어요. 즉 libfoo.so.x.y.z 파일이고, 이 공유 라이브러리는 libfoo.so.x에 심볼릭 링크돼요. 기본적으로 Raku는 그 파일이 유일하게 존재하는 경우 그것을 선택해요. 그래서 항상 버전을 포함하는 것이 더 안전하고 권장돼요:

sub some_argless_function() is native('foo', v1.2.3) { * }

ABI/API 버전 섹션을 참고하세요.

NativeCall 헬퍼 모듈 (NativeCall helper module)

비코어(non-core) Raku 모듈인 App::GPTrixie에는 gptrixie라는 Raku 프로그램이 있어서, 새 NativeCall 프로젝트를 시작할 때 시작 코드 프레임워크를 만들어줘요. 스스로를 _C 헤더로부터 NativeCall 코드를 생성하는 도구_라고 설명하며, GitHub 저장소는 여기예요. 현재는 C 언어만 지원하지만 C++ 지원이 계획돼 있어요.

이름 바꾸기 (Changing names)

가끔 Raku 서브루틴의 이름을 로드하고 있는 라이브러리의 이름과 다르게 만들고 싶을 때가 있어요. 이름이 길거나, 대소문자가 다르거나, 만들려는 모듈 맥락에서 다루기 어렵거나 할 수 있죠.

NativeCall은 symbol 트레잇을 제공해서, Raku 서브루틴 이름과는 다를 수 있는 라이브러리 안 네이티브 루틴의 이름을 지정할 수 있어요.

unit module Foo;
use NativeCall;
our sub init() is native('foo') is symbol('FOO_INIT') { * }

libfoo 안에는 FOO_INIT이라는 루틴이 있어요. 하지만 Foo라는 모듈을 만들고 있고, 그 루틴을 Foo::FOO_INIT 대신 Foo::init으로 부르고 싶어요. 그래서 symbol 트레잇으로 libfoo 안 심볼의 이름을 지정하고 서브루틴은 원하는 대로(여기서는 init) 부르는 거예요.

값 전달과 반환 (Passing and returning values)

네이티브 함수가 기대하는 인자 타입과 반환 타입을 전달하기 위해 일반 Raku 시그니처와 returns 트레잇을 사용해요. 예시를 볼게요.

use NativeCall;
sub add(int32, int32) returns int32 is native("calculator") { * }

여기서는 함수가 32비트 정수 두 개를 받고 32비트 정수를 반환한다고 선언했어요. 전달할 수 있는 다른 타입은 네이티브 타입 페이지에서 찾을 수 있어요. returns 트레잇이 없으면 C void 반환을 나타낼 수 있다는 점에 주의하세요. 실제 반환은 Mu 타입 객체예요.

void 타입은 Pointer 매개변수화 안에서만 쓰고, 그 외에는 쓰지 마세요.

문자열에는 마샬링(marshaling) 방법에 대한 추가 힌트를 주는 encoded 트레잇이 있어요.

use NativeCall;
sub message_box(Str is encoded('utf8')) is native('gui') { * }

문자열 반환 타입의 마샬링 방법을 지정하려면 이 트레잇을 루틴 자체에 적용하면 돼요.

use NativeCall;
sub input_box() returns Str is encoded('utf8') is native('gui') { * }

NULL 문자열 포인터는 Str 타입 객체로 전달할 수 있고, NULL 반환도 타입 객체로 표현돼요.

C 함수가 데이터의 수명(lifetime)을 함수 호출보다 길게 요구한다면, 예를 들어 CArray[uint8] 버퍼 같은 걸 포인터로 전달한다면, 그 버퍼를 뒷받침하는 CArray[uint8] Raku 객체가 C 함수가 요구하는 동안 파괴되지 않도록 보장해야 해요.

특히 문자열에서 이 점이 중요해요. (보통 선호되는) is encoded 방식으로 문자열을 전달하면 호출 후 자동으로 파괴되는 임시 객체만 만들기 때문이에요. 이 경우 인자는 수동으로 인코딩해야 해요:

use NativeCall;
# void set_foo(const char *)
# Records a char pointer for later usage
sub set_foo(CArray[uint8]) is native('foo') { * }
# void use_foo(void)
# Uses the pointer stored by set_foo()
sub use_foo() is native('foo') { * }

my $string = "FOO";
# Manually marshal the $string into $array. Take care that $array
# is not destroyed (e.g. by going out of scope) while the library
# still holds on to it, after set_foo().
#
# $string.encode takes care of UTF-8 encoding. If $array is used
# as a string by the native function, don't forget to append the
# NUL byte that terminates a C string: ------------v
my $array = CArray[uint8].new($string.encode.list, 0);

set_foo($array);
# ...
use_foo();
# It's fine if $array goes out of scope starting from here.

네이티브 표현형 지정하기 (Specifying the native representation)

네이티브 함수로 작업할 때, 사용될 네이티브 데이터 구조의 종류를 지정해야 하는 경우가 있어요. 이때 쓰는 용어가 is repr이에요.

use NativeCall;

class timespec is repr('CStruct') {
    has uint32 $.tv_sec;
    has long $.tv_nanosecs;
}

sub clock_gettime(uint32 $clock-id, timespec $tspec --> uint32) is native { * };

my timespec $this-time .=new;

my $result = clock_gettime( 0, $this-time);

say "$result, $this-time"; # OUTPUT: «0, timespec<65385480>␤»

우리가 부르는 원래 함수 clock_gettime은 두 번째 인자로 timespec 구조체에 대한 포인터를 사용해요. 우리는 여기서 그걸 클래스로 선언하되, 표현형을 is repr('CStruct')로 지정해 C 데이터 구조에 대응한다는 것을 나타내요. 그 클래스의 객체를 만들면 clock_gettime이 기대하는 바로 그 포인터 형태를 만드는 거예요. 이렇게 해서 데이터가 네이티브 인터페이스와 매끄럽게 오갈 수 있어요.

포인터 기본 사용법 (Basic use of pointers)

네이티브 함수의 시그니처가 어떤 네이티브 타입(int32, uint32 등)에 대한 포인터를 필요로 한다면, 인자를 is rw로 선언하기만 하면 돼요:

use NativeCall;
# C prototype is void my_version(int *major, int *minor)
sub my_version(int32 is rw, int32 is rw) is native('foo') { * }
my_version(my int32 $major, my int32 $minor); # Pass a pointer to

Pointer 클래스를 사용해 포인터를 사용/전달하고 싶을 때는 선언할 때 포인터를 만들어야 해요. 그러면 그 포인터가 무엇을 가리키는지 지정하지 않아도 돼요. 예시를 볼게요:

use NativeCall;
# C prototype is void create_object(void **object)
sub create_object(Pointer is rw) is native('foo') { * }
my Pointer $p = Pointer.new();
create_object($p); # pointer is set by create_object

Pointer 클래스는 타입과 함께 쓸 수도 있어요. 예를 들어 Pointer[Str]처럼요. Str은 문자열에 대한 포인터(C/C++의 char *)라는 점에 주의하세요. 그래서 그 경우 Pointer[Str]이나 Str을 쓸 수 있어요.

가끔 C 라이브러리로부터 포인터(예: 파일 핸들)를 받아와야 할 때가 있어요. 그것이 무엇을 가리키는지는 상관없고 그냥 붙잡아 두기만 하면 돼요. Pointer 타입이 바로 이걸 위한 것이에요.

use NativeCall;
sub Foo_init() returns Pointer is native("foo") { * }
sub Foo_free(Pointer) is native("foo") { * }

이것도 문제없지만, Pointer보다 더 나은 이름의 타입으로 작업하고 싶을 수 있어요. CPointer 표현형을 가진 어떤 클래스든 이 역할을 할 수 있다는 게 밝혀졌어요. 즉 핸들로 동작하는 라이브러리를 이런 클래스로 노출할 수 있어요:

use NativeCall;

class FooHandle is repr('CPointer') {
    # Here are the actual NativeCall functions.
    sub Foo_init() returns FooHandle is native("foo") { * }
    sub Foo_free(FooHandle) is native("foo") { * }
    sub Foo_query(FooHandle, Str) returns int8 is native("foo") { * }
    sub Foo_close(FooHandle) returns int8 is native("foo") { * }

    # Here are the methods we use to expose it to the outside world.
    method new {
        Foo_init();
    }

    method query(Str $stmt) {
        Foo_query(self, $stmt);
    }

    method close {
        Foo_close(self);
    }

    # Free data when the object is garbage collected.
    submethod DESTROY {
        Foo_free(self);
    }
}

CPointer 표현형은 C 포인터를 담는 것 외에는 아무것도 못 한다는 점을 기억하세요. 즉 여러분의 클래스는 추가 속성을 가질 수 없어요. 하지만 단순한 라이브러리에 대해서는 객체지향 인터페이스를 노출하는 깔끔한 방법이 될 수 있어요.

빈 클래스를 항상 가질 수도 있어요:

class DoorHandle is repr('CPointer') { }

그리고 그냥 Pointer처럼 클래스를 사용하면 되는데, 더 나은 타입 안전성과 더 읽기 좋은 코드를 얻을 수 있어요.

다시 한번, 타입 객체는 NULL 포인터를 나타내는 데 사용돼요.

함수 포인터 (Function pointers)

C 라이브러리는 함수에 대한 반환 값으로, 그리고 구조체나 유니온 같은 구조의 멤버로 C 함수에 대한 포인터를 노출할 수 있어요.

함수 f가 반환한 함수 포인터 $fptr을, 원하는 함수 매개변수와 반환 값을 정의하는 시그니처를 사용해 호출하는 예시예요:

sub f() returns Pointer is native('mylib') { * }

my $fptr    = f();
my &newfunc = nativecast(:(Str, size_t --> int32), $fptr);

say newfunc("test", 4);

배열 (Arrays)

NativeCall은 배열을 어느 정도 지원해요. 머신 크기의 정수, double, 문자열, 크기 지정 숫자 타입, 포인터 배열, 구조체 배열, 배열의 배열과 함께 동작하도록 제한돼 있어요.

그런데 Raku 배열은 무엇보다 지연(laziness)을 지원하는데, C 배열과는 메모리 배치가 근본적으로 달라요. 따라서 NativeCall 라이브러리는 훨씬 더 원시적인 CArray 타입을 제공하며, C 배열로 작업할 때는 이것을 사용해야 해요.

C 배열을 전달하는 예시예요:

sub RenderBarChart(Str, int32, CArray[Str], CArray[num64]) is native("chart") { * }
my @titles := CArray[Str].new;
@titles[0]  = 'Me';
@titles[1]  = 'You';
@titles[2]  = 'Hagrid';
my @values := CArray[num64].new;
@values[0]  = 59.5e0;
@values[1]  = 61.2e0;
@values[2]  = 180.7e0;
RenderBarChart('Weights (kg)', 3, @titles, @values);

@titles할당(assignment)이 아니라 바인딩(binding)을 썼다는 점을 눈여겨보세요! 할당하면 값이 Raku 배열에 들어가서 제대로 동작하지 않아요. 이 모든 게 부담스럽다면, @ 시길에 대해 알던 것을 잊고 NativeCall을 쓸 때는 쭉 $만 쓰세요.

use NativeCall;
my $titles = CArray[Str].new;
$titles[0] = 'Me';
$titles[1] = 'You';
$titles[2] = 'Hagrid';

배열의 반환 값을 얻는 것도 똑같이 동작해요.

일부 라이브러리 API는 배열을 C 함수가 채울 버퍼로 받아, 예를 들어 실제로 채워진 항목 수를 반환할 수 있어요:

use NativeCall;
sub get_n_ints(CArray[int32], int32) returns int32 is native('ints') { * }

이런 경우, CArray를 네이티브 서브루틴에 전달하기 전에 채워질 요소 수 이상을 갖도록 하는 것이 중요해요. 그렇지 않으면 C 함수가 Raku의 메모리를 덮어써서 예측할 수 없는 동작을 일으킬 수 있어요:

my $number_of_ints = 10;
my $ints = CArray[int32].allocate($number_of_ints); # instantiates an array with 10 elements
my $n = get_n_ints($ints, $number_of_ints);

참고: allocate는 Rakudo 2018.05에서 도입됐어요. 그 전에는 배열을 어떤 개수의 요소로 확장하려면 아래 메커니즘을 써야 했어요:

my $ints = CArray[int32].new;
my $number_of_ints = 10;
$ints[$number_of_ints - 1] = 0; # extend the array to 10 items

배열이 메모리를 어떻게 관리하는지 이해하는 것이 중요해요. 배열을 직접 만들면 원하는 대로 요소를 추가할 수 있고 필요에 따라 자동으로 확장돼요. 하지만 이로 인해 배열이 메모리에서 이동될 수 있어요(기존 요소에 할당하는 것은 절대 이동을 일으키지 않아요). 다시 말해 배열을 C 라이브러리에 전달한 후 그 배열을 만지작거린다면 뭘 하고 있는지 잘 알아야 해요.

반대로 C 라이브러리가 배열을 반환할 때는 NativeCall이 메모리를 관리할 수 없고, 배열이 어디서 끝나는지도 모르는 상태예요. 아마 라이브러리 API의 어떤 것이 그걸 알려줄 거예요(예를 들어 null 요소를 보면 더 읽지 말아야 한다는 걸 아는 식으로). 여기서 NativeCall은 여러분에게 어떤 보호도 제공할 수 없다는 점에 주의하세요 — 잘못하면 세그멘테이션 폴트(segfault)나 메모리 손상이 발생해요. 이것은 NativeCall의 단점이 아니라, 무서운 네이티브 세계가 동작하는 방식이에요. 겁났나요? 여기 안아드릴게요. 행운을 빌어요!

CArray 메서드 (CArray methods)

모든 Raku 인스턴스에서 쓸 수 있는 일반 메서드 외에도, CArray는 Raku 관점에서 상호작용하는 데 쓸 수 있는 다음 메서드들을 제공해요:

  • elems — 배열 안의 요소 수를 제공.
  • AT-POS — 주어진 위치(0부터 시작)의 특정 요소를 제공. 이 메서드는 직접 쓰기 위한 것이 아니라 첨자 표기 []로 호출되도록 만들어진 거예요.
  • list — 네이티브 배열 이터레이터에서 List를 만들어 배열 안의 요소 목록을 제공.

예를 들어 다음 간단한 코드를 볼게요:

use NativeCall;

my $native-array = CArray[int32].new( 1, 2, 3, 4, 5 );
say 'Number of elements: ' ~ $native-array.elems;

# walk the array
for $native-array.list -> $elem {
    say "Current element is: $elem";
}

# get every element by its index-based position
for 0..$native-array.elems - 1 -> $position {
    say "Element at position $position is "
          ~ $native-array[ $position ];
}

이 코드는 다음 출력을 만들어요:

Number of elements: 5
Current element is: 1
Current element is: 2
Current element is: 3
Current element is: 4
Current element is: 5
Element at position 0 is 1
Element at position 1 is 2
Element at position 2 is 3
Element at position 3 is 4
Element at position 4 is 5

CArray 하위 배열 (CArray sub-arrays)

주어진 CArray의 하위 배열을 사용하는 것은 포인터 산술의 간단한 연습이에요. 여러분 것으로 만들려면 다음 예시를 자유로이 사용하세요:

sub subarray (
  $a, #= The CArray to use
  $o  #= The offset into the array where the subarray should start
) is export {
  my $b = nativecast(Pointer[$a.of], $a);
  nativecast(CArray[$a.of], $b.add($o) );
}

구조체 (Structs)

표현 다형성(representation polymorphism) 덕분에, 평범해 보이는 Raku 클래스를 선언해서 그 속성을 C 컴파일러가 비슷한 구조체 정의에서 배치하는 방식과 똑같이 저장하도록 할 수 있어요. 필요한 건 repr 트레잇을 사용하는 것뿐이에요:

class Point is repr('CStruct') {
    has num64 $.x;
    has num64 $.y;
}

속성은 NativeCall이 구조체 필드로 마샬링할 줄 아는 타입만 될 수 있어요. 현재 구조체는 머신 크기의 정수, double, 문자열, 그리고 다른 NativeCall 객체(CArray, CPointerCStruct repr을 쓰는 것들)를 담을 수 있어요. 그 외에는 클래스에서 하는 일반적인 일들을 할 수 있어요. 일부 속성을 롤에서 가져오거나 다른 클래스에서 상속받을 수도 있고요. 메서드도 완전히 괜찮아요. 마음껏 쓰세요!

CStruct 객체는 참조로 네이티브 함수에 전달되고, 네이티브 함수도 참조로 CStruct 객체를 반환해야 해요. 이 참조들의 메모리 관리 규칙은 배열의 규칙과 매우 비슷하지만, 구조체는 크기가 조정되지 않으므로 더 단순해요. 구조체를 만들면 메모리가 관리되고, CStruct 인스턴스를 가리키는 변수가 사라지면 GC가 처리할 때 메모리가 해제돼요. CStruct 기반 타입이 네이티브 함수의 반환 타입으로 쓰이면, GC가 메모리를 관리해주지 않아요.

NativeCall은 현재 객체 멤버를 컨테이너에 넣지 않아서, =로 새 값을 할당해도 동작하지 않아요. 대신 비공개 멤버에 :=로 새 값을 바인딩해야 해요:

class MyStruct is repr('CStruct') {
    has CArray[num64] $!arr;
    has Str $!str;
    has Point $!point; # Point is a user-defined class shown above

    submethod TWEAK {
        my $arr := CArray[num64].new;
        $arr[0] = 0.9e0;
        $arr[1] = 0.2e0;
        $!arr := $arr;
        $!str := 'Raku is fun';
        $!point := Point.new;
    }
}

이쯤에서 예상했듯, NULL 포인터는 구조체 타입의 타입 객체로 표현돼요.

CUnion (CUnion s)

마찬가지로, 속성을 C 컴파일러가 비슷한 union 정의에서 배치하는 방식과 똑같이 저장하는 Raku 클래스를 선언할 수 있어요. CUnion 표현형을 써서요:

use NativeCall;

class MyUnion is repr('CUnion') {
    has int32 $.flags32;
    has int64 $.flags64;
}

say nativesizeof(MyUnion.new);  # OUTPUT: «8␤»
                                # ie. max(sizeof(MyUnion.flags32), sizeof(MyUnion.flags64))

HAS

CStruct, CUnion, CArray는 차례로 주변의 CStructCUnion에 의해 참조되거나 **포함(embedded)**될 수 있어요. 전자를 말할 때는 평소처럼 has를 쓰고, 후자를 말할 때는 HAS 선언자를 대신 써요.

배열을 포함하려면 배열 크기가 고정되도록 차원(dimensions)을 제공해야 해요.

class MyStruct is repr('CStruct') {
    has Point $.point;  # referenced
    has int32 $.flags;
}

say nativesizeof(MyStruct.new);  # OUTPUT: «16␤»
                                 # ie. Point* + int32 + padding
                                 # ie. 8      + 4     + 4

class MyStruct2 is repr('CStruct') {
    HAS Point $.point;  # embedded
    has int32 $.flags;
    HAS int32 @.b[4] is CArray;
}

say nativesizeof(MyStruct2.new);  # OUTPUT: «40␤»
                                  # ie. Point + int32 + padding + 4 * int32
                                  # ie. 16    + 4     + 4       + 4 * 4

메모리 관리에 관한 참고 (Notes on memory management)

구조체로 사용할 구조체를 할당할 때는 C 함수에서 자신의 메모리를 할당하도록 하세요. Str/char*를 미리 할당해야 하는 C 함수에 구조체를 전달한다면, 함수에 구조체를 전달하기 전에 Str 타입 변수의 컨테이너를 할당해 두어야 해요.

여러분의 Raku 코드에서... (In your Raku code...)

class AStringAndAnInt is repr("CStruct") {
  has Str $.a_string;
  has int32 $.an_int32;

  sub init_struct(AStringAndAnInt is rw, Str, int32) is native('simple-struct') { * }

  submethod BUILD(:$a_string, :$an_int) {
    init_struct(self, $a_string, $an_int);
  }
}

이 코드에서는 먼저 멤버 $.a_string$.an_int32를 설정해요. 그 다음 init() 메서드를 감싸기 위한 init_struct() 함수를 선언하고, 이 함수가 BUILD에서 호출되어 생성된 객체를 반환하기 전에 값을 실제로 할당해요.

BUILD가 $.an_int32 같은 네이티브 타입의 속성을 바인딩한다는 점을 주목하세요. 네이티브 타입은 항상 이렇게 바인딩할 수 있어요.

여러분의 C 코드에서... (In your C code...)

typedef struct a_string_and_an_int32_t_ {
  char *a_string;
  int32_t an_int32;
} a_string_and_an_int32_t;

이게 구조체예요. char *가 있다는 점을 눈여겨보세요.

void init_struct(a_string_and_an_int32_t *target, char *str, int32_t int32) {
  target->an_int32 = int32;
  target->a_string = strdup(str);

  return;
}

이 함수에서는 정수를 값으로 할당하고 문자열을 참조로 전달해서 C 구조체를 초기화해요. 함수는 문자열을 복사하면서 구조체 안 char *a_string이 가리키는 메모리를 할당해요. (메모리 누수를 피하려면 메모리 해제도 관리해야 한다는 점을 잊지 마세요.)

# A long time ago in a galaxy far, far away...
my $foo = AStringAndAnInt.new(a_string => "str", an_int => 123);
say "foo is {$foo.a_string} and {$foo.an_int32}";
# OUTPUT: «foo is str and 123␤»

타입 지정 포인터 (Typed pointers)

Pointer에 타입을 매개변수로 전달해 지정할 수 있어요. 네이티브 타입뿐 아니라 CArrayCStruct로 정의된 타입에서도 동작해요. NativeCall은 new를 호출해도 그 메모리를 암시적으로 할당하지 않아요. 이는 주로 C 루틴이 포인터를 반환하거나 포인터가 CStruct 안에 포함된 경우에 유용해요.

use NativeCall;
sub strdup(Str $s --> Pointer[Str]) is native { * }
my Pointer[Str] $p = strdup("Success!");
say $p.deref;

포함된 타입에 접근하려면 Pointer.deref를 호출해야 해요. 위 예시에서 포인터의 타입을 선언하면 역참조할 때 타입캐스트 오류를 피할 수 있어요. 원래 strdupchar에 대한 포인터를 반환한다는 점을 기억하세요. 우리는 Pointer[Str]을 쓰고 있어요.

my Pointer[int32] $p; #For a pointer on int32;
my Pointer[MyCstruct] $p2 = some_c_routine();
my MyCstruct $mc = $p2.deref;
say $mc.field1;

네이티브 함수가 요소 배열에 대한 포인터를 반환하는 것은 꽤 흔해요. 타입 지정 포인터는 배열로 역참조해서 개별 요소를 얻을 수 있어요.

my $n = 5;
# returns a pointer to an array of length $n
my Pointer[Point] $plot = some_other_c_routine($n);
# display the 5 elements in the array
for 1 .. $n -> $i {
    my $x = $plot[$i - 1].x;
    my $y = $plot[$i - 1].y;
    say "$i: ($x, $y)";
}

포인터는 배열의 연속 요소를 가리키도록 갱신할 수도 있어요:

my Pointer[Point] $elem = $plot;
# show differences between successive points
for 1 ..^ $n {
    my Point $lo = $elem.deref;
    ++$elem; # equivalent to $elem = $elem.add(1);
    my Point $hi = (++$elem).deref;
    my $dx = $hi.x = $lo.x;
    my $dy = $hi.y = $lo.y;
    say "$_: delta ($dx, $dy)";
}

void 포인터도 Pointer[void]로 선언해 쓸 수 있어요. 자세한 내용은 네이티브 타입 문서를 참고하세요.

문자열 (Strings)

명시적 메모리 관리 (Explicit memory management)

전달된 문자열을 캐시하는 C 코드가 있다고 해볼게요:

#include <stdlib.h>

static char *__VERSION;

char *
get_version()
{
    return __VERSION;
}

char *
set_version(char *version)
{
    if (__VERSION != NULL) free(__VERSION);
    __VERSION = version;
    return __VERSION;
}

get_versionset_version의 바인딩을 작성한다면 처음에는 이렇게 생기겠지만, 의도대로 동작하지 않아요:

sub get_version(--> Str)     is native('./version') { * }
sub set_version(Str --> Str) is native('./version') { * }

say set_version('1.0.0'); # 1.0.0
say get_version;          # Differs on each run
say set_version('1.0.1'); # Double free; segfaults

이 코드는 두 번째 set_version 호출에서 세그폴트가 나요. 가비지 컬렉터가 이미 해제한 뒤 첫 호출에서 전달한 문자열을 다시 free하려고 하기 때문이에요. 네이티브 함수에 전달한 문자열을 가비지 컬렉터가 해제하지 않게 하려면 explicitly-manage를 함께 쓰세요:

say set_version(explicitly-manage('1.0.0')); # 1.0.0
say get_version;                             # 1.0.0
say set_version(explicitly-manage('1.0.1')); # 1.0.1
say get_version;                             # 1.0.1

명시적으로 관리되는 문자열의 모든 메모리 관리는 메모리 누수를 막기 위해 C 라이브러리 자체나 NativeCall API가 처리해야 한다는 점을 명심하세요.

버퍼와 블롭 (Buffers and blobs)

BlobBuf는 바이너리 데이터를 저장하는 Raku의 방식이에요. 네이티브 함수·데이터 구조와 직접은 아니지만 데이터를 교환하는 데 쓸 수 있어요. nativecast를 사용해야 해요.

my $blob = Blob.new(0x22, 0x33);
my $src = nativecast(Pointer, $blob);

$srcPointer를 받는 어떤 네이티브 함수의 인자로든 쓸 수 있어요. 그 반대, 즉 Pointer가 가리키는 값을 Buf에 넣거나 Blob을 초기화하는 데 쓰는 것은 직접 지원되지 않아요. 그런 연산을 하려면 NativeHelpers::Blob을 쓰는 게 좋을 수 있어요.

my $esponja = blob-from-pointer( $inter, :2elems, :type(Blob[int8]));
say $esponja;

함수 인자 (Function arguments)

NativeCall은 함수를 인자로 받는 네이티브 함수도 지원해요. 그 한 예로 이벤트 구동 시스템에서 콜백으로 함수 포인터를 사용하는 경우예요. NativeCall로 이 함수들을 바인딩할 때는 코드 매개변수의 제약 조건으로서 동등한 시그니처만 제공하면 돼요. 다만 NativeCall의 경우에는 Rakudo 2019.07 기준으로, 함수 인자와 시그니처 사이의 공백과 일반 Signature 리터럴의 콜론이 생략돼요:

use NativeCall;
# void SetCallback(int (*callback)(const char *))
my sub SetCallback(&callback (Str --> int32)) is native('mylib') { * }

참고: 이렇게 Raku 콜백에 전달되는 값의 메모리 관리는 네이티브 코드가 담당해요. 다시 말해 NativeCall은 콜백에 전달된 문자열을 free()하지 않아요.

네이티브 콜백으로 전달되는 어떤 코드든 자체 예외를 처리하고, 해당되면 그것을 호출한 네이티브 코드에 적절한 오류 값을 반환하는 것이 중요해요. 네이티브 콜백 밖으로 예외를 던지는 것은 허용되지 않으며, 그렇게 하면 프로세스가 종료돼요.

가변 인자 함수 (Variadic functions)

가변 인자(variadic) 함수를 호출하려면 고정 인자를 평소처럼 지정하고 슬러피(slurpy) 인자를 추가해요. 가변 인자 함수를 호출할 때는 원하는 만큼 인자를 제공하면 돼요. 가변 인자의 타입은 전달된 값을 보고 추론돼요.

use NativeCall;

# Sums the passed int arguments. First arg is the count of the variadic args.
sub sum_int_things(int32, **@varargs) returns int32 is native('mylib') { * }
say sum_things(3, 1, 2, 3);  # 6

그러므로 인자를 의도한 타입으로 먼저 캐스팅해야 해요.

my $input = prompt("Give a number to add 5 to: ");
say sum_things(2, $input.Int, 5);

포인터 전달은 Pointer.to() 메서드로 동작해요:

use NativeCall;

# Writes a 5 into all passed pointers.
sub write_5_to_ints(int32, **@varargs) is native('mylib') { * }
my uint32 $number1;
my uint32 $number2;
write_5_to_ints(2, Pointer.to($number1), Pointer.to($number2));
say "One: $number1, Two: $number2"; #One: 5, Two: 5

라이브러리 경로와 이름 (Library paths and names)

native 트레잇은 라이브러리 이름, 전체 경로, 또는 둘 중 하나를 반환하는 서브루틴을 받아들여요. 라이브러리 이름을 쓸 때는 이름 앞에 lib가 붙고 뒤에 .so가 붙는 것으로 가정되며(윈도우에서는 lib가 앞에 붙지 않고 뒤에 .dll만 붙음), LD_LIBRARY_PATH(윈도우에서는 PATH) 환경 변수의 경로에서 검색돼요.

use NativeCall;
constant LIBMYSQL = 'mysqlclient';
constant LIBFOO = '/usr/lib/libfoo.so.1';
sub LIBBAR {
    my $path = qx/pkg-config --libs libbar/.chomp;
    $path ~~ s/\/[[\w+]+ % \/]/\0\/bar/;
    $path
}
# and later

sub mysql_affected_rows returns int32 is native(LIBMYSQL) { * };
sub bar is native(LIBFOO) { * }
sub baz is native(LIBBAR) { * }

'./foo' 같은 불완전한 경로를 넣을 수도 있는데, 그러면 NativeCall이 플랫폼 사양에 따라 올바른 확장자를 자동으로 붙여줘요. 이 확장을 끄고 싶다면 문자열을 블록의 본문으로 전달하면 돼요.

sub bar is native({ './lib/Non Standard Naming Scheme' }) { * }

주의하세요: native 트레잇과 constant는 컴파일 타임에 평가돼요. 동적 변수에 의존하는 constant를 쓰지 마세요. 예를 들어:

# WRONG:
constant LIBMYSQL = %*ENV<P6LIB_MYSQLCLIENT> || 'mysqlclient';

이러면 컴파일 타임에 주어진 값을 유지할 거예요. 모듈이 사전 컴파일(precompiled)될 것이고 LIBMYSQL은 모듈이 사전 컴파일될 때 얻게 된 값을 유지할 거예요.

ABI/API 버전

native('foo')라고 적으면 NativeCall은 유닉스 계열 시스템에서 libfoo.so를 검색해요 (OS X에서는 libfoo.dynlib, win32에서는 foo.dll). 대부분의 현대 시스템에서 공유 라이브러리에 항상 API/ABI 버전을 제공하는 것이 권장되므로, libfoo.so는 개발 패키지에서만 제공되는 심볼릭 링크가 되는 경우가 많아요. 그래서 여러분이나 모듈 사용자가 개발 패키지를 설치해야 할 수도 있어요.

이를 피하기 위해 native 트레잇으로 API/ABI 버전을 지정할 수 있어요. 전체 버전일 수도 있고 일부만일 수도 있어요. (주 버전(Major)을 고수하세요. 일부 BSD 코드는 부 버전(Minor)을 신경 쓰지 않아요.)

use NativeCall;
sub foo1 is native('foo', v1) { * } # Will try to load libfoo.so.1
sub foo2 is native('foo', v1.2.3) { * } # Will try to load libfoo.so.1.2.3

my List $lib = ('foo', 'v1');
sub foo3 is native($lib) { * }

루틴 (Routine)

native 트레잇은 Callable도 인자로 받아들여, 로드할 라이브러리 파일을 찾는 방식을 직접 처리할 수 있게 해줘요.

use NativeCall;
sub foo is native(sub {'libfoo.so.42'}) { * }

이것은 서브루틴이 처음 호출될 때만 호출돼요.

표준 라이브러리 호출 (Calling into the standard library)

이미 로드된 C 함수를 호출하고 싶다면 — 표준 라이브러리든 여러분 프로그램에서든 — 값을 생략해서 is native라고 적으면 돼요.

예를 들어 유닉스 계열 운영체제에서 아래 코드로 현재 사용자의 홈 디렉터리를 출력할 수 있어요:

use NativeCall;
my class PwStruct is repr('CStruct') {
    has Str $.pw_name;
    has Str $.pw_passwd;
    has uint32 $.pw_uid;
    has uint32 $.pw_gid;
    has Str $.pw_gecos;
    has Str $.pw_dir;
    has Str $.pw_shell;
}
sub getuid()              returns uint32   is native { * };
sub getpwuid(uint32 $uid) returns PwStruct is native { * };

say getpwuid(getuid()).pw_dir;

물론 $*HOME이 훨씬 쉬운 방법이긴 해요 :-)!

내보내진 변수 (Exported variables)

라이브러리가 내보내는 변수 — "global" 또는 "extern" 변수라고도 함 — 는 cglobal로 접근할 수 있어요. 예를 들어:

my $var := cglobal('libc.so.6', 'errno', int32)

이 코드는 $var에 새 Proxy 객체를 바인딩하는데, 그 객체는 모든 접근을 libc.so.6 라이브러리가 내보내는 "errno"라는 정수 변수로 리다이렉트해요.

C++ 지원 (C++ support)

NativeCall은 https://github.com/rakudo/rakudo/blob/master/t/04-nativecall/13-cpp-mangling.t(그리고 관련 C++ 파일)에서 보여주는 대로 C++의 클래스와 메서드 사용을 지원해요. 현재 C 지원만큼 테스트·개발되지 않았다는 점을 유의하세요.

헬퍼 함수 (Helper functions)

NativeCall 라이브러리는 네이티브 라이브러리의 데이터로 작업하는 데 도움을 주는 몇몇 서브루틴을 내보내요.

sub nativecast

sub nativecast($target-type, $source) is export(:DEFAULT)

이것은 Pointer $source$target-type의 객체로 _캐스팅_해요. 소스 포인터는 보통 포인터를 반환하는 네이티브 서브루틴 호출에서 얻거나 struct의 멤버로서 얻는데, 예를 들어 C 라이브러리 정의에서 void *로 지정될 수 있어요. 덜 구체적인 타입의 포인터를 더 구체적인 타입으로 캐스팅할 수도 있어요.

특수한 경우로, Signature$target-type으로 주어지면 subroutine이 반환되는데, 그 서브루틴은 native 트레잇으로 선언된 서브루틴과 같은 방식으로 $source가 가리키는 네이티브 함수를 호출해요. 이 내용은 Function Pointers에서 설명해요.

sub cglobal

sub cglobal($libname, $symbol, $target-type) is export is rw

이것은 지정된 라이브러리가 노출하는 extern 이름인 $symbol에 접근을 제공하는 Proxy 객체를 반환해요. 라이브러리는 native 트레잇에 지정하는 것과 같은 방식으로 지정할 수 있어요.

sub nativesizeof

sub nativesizeof($obj) is export(:DEFAULT)

이것은 주어진 객체의 바이트 크기를 반환해요. Csizeof와 동등하다고 생각하면 돼요. 객체는 int64num64 같은 내장 네이티브 타입, CArray, 또는 reprCStruct, CUnion, CPointer인 클래스일 수 있어요.

sub explicitly-manage

sub explicitly-manage($str) is export(:DEFAULT)

이것은 주어진 Str에 대한 객체를 반환해요. 반환된 문자열이 NativeCall 서브루틴에 전달되면 런타임의 가비지 컬렉터가 해제하지 않아요.

예시 (Examples)

특정 예시와, 위 예시를 특정 플랫폼에서 사용하는 방법.

PostgreSQL

DBIish의 PostgreSQL 예시는 NativeCall 라이브러리와 is native를 사용해 Windows에서 네이티브 _putenv 함수 호출을 이용해요.

MySQL

참고: Debian은 Stretch 버전부터 내부적으로 MySQL을 MariaDB로 대체했어요. 그래서 MySQL을 설치하려면 기본 저장소 대신 MySQL APT repository를 사용해야 해요.

DBIish의 MySQL 예시를 쓰려면 MySQL 서버를 로컬에 설치해야 해요. Debian 계열 시스템에서는 다음과 같이 설치할 수 있어요:

wget https://dev.mysql.com/get/mysql-apt-config_0.8.10-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.10-1_all.deb # Don't forget to select 5.6.x
sudo apt-get update
sudo apt-get install mysql-community-server -y
sudo apt-get install libmysqlclient18 -y

예시를 시도하기 전에 시스템을 이렇게 준비하세요:

$ mysql -u root -p
SET PASSWORD = PASSWORD('sa');
DROP DATABASE test;
CREATE DATABASE test;

Microsoft Windows API

Windows API 호출 예시예요:

use NativeCall;

sub MessageBoxA(int32, Str, Str, int32)
    returns int32
    is native('user32')
    { * }

MessageBoxA(0, "We have NativeCall", "ohai", 64);

wchar_t

wchar_t 문자열을 전달·수신하는 것은 트레잇으로 Str 인코딩을 전달해서 동작해요:

use NativeCall;

# Using the wide character variant (notice the trailing "W" in the name)
sub MessageBoxW(int32, Str is encoded('utf16'), Str is encoded('utf16'), int32
    --> int32) is native('user32') { * }

MessageBoxW(0, "We have NativeCall", "ohai", 64);

네이티브 함수가 설정하는 문자열을 포인터로 전달할 때는 평범한 Pointer를 써야 해요:

use NativeCall;

sub make-guid($val) {
    my $buf = Buf[uint8].new;
    for ^16 Z $val.subst('-', :g).comb(2) -> ($i, $byte-text) {
        my $byte = $byte-text.parse-base(16).Int;
        $buf.write-uint8: $i, $byte;
    }
    $buf
}
my $local-appdata-guid = make-guid('F1B32785-6FBA-4FCF-9D55-7B8E7F157091');

sub SHGetKnownFolderPath(
  Buf[uint8]      $rfid,
  uint32          $dwFlags,
  Pointer         $hToken,
  Pointer[uint16] $ppszPath is rw
  --> int32) is native('Shell32') { * }

my Pointer[uint16] $path-pointer .= new;
SHGetKnownFolderPath($local-appdata-guid, 0, 0, $path-pointer);

my $buf = Buf[uint8].new;
my $pos = 0;
while $path-pointer.deref() != 0 {
    $buf.write-uint16($pos, $path-pointer.deref());
    $pos += 2;
    $path-pointer++;
}
$buf.write-uint16($pos, 0);
my $path = $buf.decode('utf16');

C 함수 호출에 관한 짧은 튜토리얼 (Short tutorial on calling a C function)

이것은 표준 함수를 호출하고 반환된 정보를 Raku 프로그램에서 사용하는 예시예요.

getaddrinfo는 네트워크 노드(예: google.com)에 대한 네트워크 정보를 얻는 POSIX 표준 함수예요. NativeCall의 여러 요소를 보여주기 때문에 살펴볼 만한 흥미로운 함수예요.

Linux 매뉴얼은 이 C 호출 가능 함수에 대해 다음 정보를 제공해요:

int getaddrinfo(const char *node, const char *service,
       const struct addrinfo *hints,
       struct addrinfo **res);

이 함수는 성공 시 응답 코드 0을, 오류 시 1을 반환해요. 데이터는 addrinfo 요소의 연결 리스트에서 추출되며, 첫 요소는 res가 가리켜요.

NativeCall 타입 표에서 intint32라는 것을 알 수 있어요. char *는 C Str의 형태 중 하나이며 Str에 단순 매핑되는 것도 알아요. 하지만 addrinfo는 구조체이므로 우리 자신의 타입 클래스를 작성해야 해요. 함수 선언 자체는 간단해요:

sub getaddrinfo( Str $node, Str $service, Addrinfo $hints, Pointer $res is rw )
    returns int32
    is native
    { * }

$res는 함수가 써야 하는 값이므로 is rw 트레잇을 붙여야 한다는 점을 주목하세요. 라이브러리가 표준 POSIX이므로 라이브러리 이름은 타입 정의나 null일 수 있어요.

이제 구조체 Addrinfo를 처리해야 해요. Linux 매뉴얼은 이 정보를 제공해요:

struct addrinfo {
               int              ai_flags;
               int              ai_family;
               int              ai_socktype;
               int              ai_protocol;
               socklen_t        ai_addrlen;
               struct sockaddr *ai_addr;
               char            *ai_canonname;
               struct addrinfo *ai_next;
           };

int, char* 부분은 간단해요. 조사 결과 socklen_t는 아키텍처에 따라 달라질 수 있지만 최소 32비트의 부호 없는 정수예요. 그래서 socklen_tuint32 타입에 매핑할 수 있어요.

복잡한 부분은 sockaddr인데, 이는 ai_socktype이 정의되지 않았는지, INET인지, INET6인지(표준 v4 IP 주소인지 v6 주소인지)에 따라 달라져요.

그래서 C struct addrinfo에 매핑할 Raku class를 만듭니다. 그 과정에서 필요한 SockAddr용 클래스도 만듭니다.

class SockAddr is repr('CStruct') {
    has int32    $.sa_family;
    has Str      $.sa_data;
}

class Addrinfo is repr('CStruct') {
    has int32     $.ai_flags;
    has int32     $.ai_family;
    has int32     $.ai_socktype;
    has int32     $.ai_protocol;
    has int32     $.ai_addrlen;
    has SockAddr  $.ai_addr       is rw;
    has Str       $.ai_cannonname is rw;
    has Addrinfo  $.ai_next       is rw;

}

마지막 세 속성의 is rw는 이것들이 C에서 포인터로 정의됐다는 점을 반영해요.

C Struct에 매핑할 때 중요한 것은 클래스의 상태 부분, 즉 속성들의 구조예요. 하지만 클래스는 메서드를 가질 수 있고 NativeCall은 C 매핑을 위해 메서드를 '건드리지' 않아요. 즉 속성을 더 읽기 좋게 풀어내는 추가 메서드를 클래스에 더할 수 있어요. 예를 들어:

method flags {
    do for AddrInfo-Flags.enums { .key if $!ai_flags +& .value }
}

적절한 enum을 정의하면 flags는 비트로 뭉친 정수 대신 키 문자열을 반환해요.

sockaddr 구조에서 가장 유용한 정보는 소켓의 family에 따라 달라지는 노드의 주소예요. 그래서 Raku 클래스에 family에 따라 주소를 해석하는 address 메서드를 더할 수 있어요.

사람이 읽을 수 있는 IP 주소를 얻으려면 addrinfo안의 버퍼를 받아 char *를 반환하는 C 함수 inet_ntop이 있어요.

이 모든 것을 합치면 다음 프로그램이 됩니다:

#!/usr/bin/env raku

use v6;
use NativeCall;

constant \INET_ADDRSTRLEN = 16;
constant \INET6_ADDRSTRLEN = 46;

enum AddrInfo-Family (
    AF_UNSPEC                   => 0;
    AF_INET                     => 2;
    AF_INET6                    => 10;
);

enum AddrInfo-Socktype (
    SOCK_STREAM                 => 1;
    SOCK_DGRAM                  => 2;
    SOCK_RAW                    => 3;
    SOCK_RDM                    => 4;
    SOCK_SEQPACKET              => 5;
    SOCK_DCCP                   => 6;
    SOCK_PACKET                 => 10;
);

enum AddrInfo-Flags (
    AI_PASSIVE                  => 0x0001;
    AI_CANONNAME                => 0x0002;
    AI_NUMERICHOST              => 0x0004;
    AI_V4MAPPED                 => 0x0008;
    AI_ALL                      => 0x0010;
    AI_ADDRCONFIG               => 0x0020;
    AI_IDN                      => 0x0040;
    AI_CANONIDN                 => 0x0080;
    AI_IDN_ALLOW_UNASSIGNED     => 0x0100;
    AI_IDN_USE_STD3_ASCII_RULES => 0x0200;
    AI_NUMERICSERV              => 0x0400;
);

sub inet_ntop(int32, Pointer, Blob, int32 --> Str)
    is native {}

class SockAddr is repr('CStruct') {
    has uint16 $.sa_family;
}

class SockAddr-in is repr('CStruct') {
    has int16 $.sin_family;
    has uint16 $.sin_port;
    has uint32 $.sin_addr;

    method address {
        my $buf = buf8.allocate(INET_ADDRSTRLEN);
        inet_ntop(AF_INET, Pointer.new(nativecast(Pointer,self)+4),
            $buf, INET_ADDRSTRLEN)
    }
}

class SockAddr-in6 is repr('CStruct') {
    has uint16 $.sin6_family;
    has uint16 $.sin6_port;
    has uint32 $.sin6_flowinfo;
    has uint64 $.sin6_addr0;
    has uint64 $.sin6_addr1;
    has uint32 $.sin6_scope_id;

    method address {
        my $buf = buf8.allocate(INET6_ADDRSTRLEN);
        inet_ntop(AF_INET6, Pointer.new(nativecast(Pointer,self)+8),
            $buf, INET6_ADDRSTRLEN)
    }
}

class Addrinfo is repr('CStruct') {
    has int32 $.ai_flags;
    has int32 $.ai_family;
    has int32 $.ai_socktype;
    has int32 $.ai_protocol;
    has uint32 $.ai_addrNativeCalllen;
    has SockAddr $.ai_addr is rw;
    has Str $.ai_cannonname is rw;
    has Addrinfo $.ai_next is rw;

    method flags {
        do for AddrInfo-Flags.enums { .key if $!ai_flags +& .value }
    }

    method family {
        AddrInfo-Family($!ai_family)
    }

    method socktype {
        AddrInfo-Socktype($!ai_socktype)
    }

    method address {
        given $.family {
            when AF_INET {
                nativecast(SockAddr-in, $!ai_addr).address
            }
            when AF_INET6 {
                nativecast(SockAddr-in6, $!ai_addr).address
            }
        }
    }
}

sub getaddrinfo(Str $node, Str $service, Addrinfo $hints,
                Pointer $res is rw --> int32)
    is native {};

sub freeaddrinfo(Pointer)
    is native {}

sub MAIN() {
    my Addrinfo $hint .= new(:ai_flags(AI_CANONNAME));
    my Pointer $res .= new;
    my $rv = getaddrinfo("google.com", Str, $hint, $res);
    say "return val: $rv";
    if ( ! $rv ) {
        my $addr = nativecast(Addrinfo, $res);
        while $addr {
            with $addr {
                say "Name: ", $_ with .ai_cannonname;
                say .family, ' ', .socktype;
                say .address;
                $addr = .ai_next;
            }
        }
    }
    freeaddrinfo($res);
}

이 코드는 다음 출력을 만들어요:

return val: 0
Name: google.com
AF_INET SOCK_STREAM
216.58.219.206
AF_INET SOCK_DGRAM
216.58.219.206
AF_INET SOCK_RAW
216.58.219.206
AF_INET6 SOCK_STREAM
2607:f8b0:4006:800::200e
AF_INET6 SOCK_DGRAM
2607:f8b0:4006:800::200e
AF_INET6 SOCK_RAW
2607:f8b0:4006:800::200e

플랫폼별 참고 사항 (Platform Specific Notes)

MacOS — DYLD_LIBRARY_PATH는 무시됨 (MacOS - DYLD_LIBRARY_PATH is ignored)

MacOS X El Capitan 이후로 시스템 무결성 보호(System Integrity Protection, 줄여서 SIP)는 보호된 프로세스가 DYLD_LIBRARY_PATH를 포함한 여러 환경 변수를 통과시키지 못하게 해요. shebang 줄에서 자주 쓰이는 env 프로그램도 그런 프로그램 중 하나예요. 즉 env가 프로그램 호출에 관여할 때마다 DYLD_LIBRARY_PATH 변수가 지워져요. 이 효과를 우회하려면 보호된 프로세스가 관여하지 않게 하거나(어려울 수 있어요) SIP를 끄야 해요.

_Homebrew_나 MacPorts 같은 대중적인 설치 방법을 쓸 때는 보통 문제가 되지 않아요. 홈 디렉터리 같은 비표준 위치에 설치할 때 더 문제가 되기 쉽고, DYLD_LIBRARY_PATH 변수를 명시적으로 활용할 때는 확실히 문제가 돼요.

Apple의 SIP 문서brian d foy의 관련 블로그 글에서 더 자세한 내용을 확인하세요.