C와 Fortran 코드 호출하기
C와 Fortran 코드 호출하기 (Calling C and Fortran Code)
대부분의 코드는 줄리아로 쓸 수 있지만, 이미 C와 Fortran으로 작성된 훌륭하고 성숙한 수치 계산용 라이브러리가 많아요. 이 기존 코드를 쉽게 사용할 수 있도록, 줄리아는 C와 Fortran 함수를 간단하고 효율적으로 호출하게 해 줘요. 줄리아는 "보일러플레이트 없음(no boilerplate)" 철학을 가지고 있어요. 함수는 "접착(glue)" 코드, 코드 생성, 컴파일 없이 — 심지어 대화형 프롬프트에서도 — 줄리아에서 직접 호출할 수 있어요. 이는 @ccall 매크로(또는 덜 편리한 ccall 문법, ccall 문법 절 참고)로 적절한 호출을 하기만 하면 이뤄져요.
호출할 코드는 공유 라이브러리(shared library)로 사용 가능해야 해요. 대부분의 C와 Fortran 라이브러리는 이미 공유 라이브러리로 컴파일되어 배포되지만, GCC(또는 Clang)로 직접 컴파일한다면 -shared와 -fPIC 옵션을 써야 해요. 줄리아의 JIT가 생성하는 기계 명령어는 네이티브 C 호출과 같아서, 결과 오버헤드는 C 코드에서 라이브러리 함수를 호출하는 것과 같아요. [1]
기본적으로 Fortran 컴파일러는 맹글된 이름(mangled names)을 생성해요 (예를 들어 함수 이름을 소문자나 대문자로 바꾸고 종종 밑줄을 추가). 그래서 Fortran 함수를 호출하려면 당신의 Fortran 컴파일러가 따르는 규칙에 대응하는 맹글된 식별자를 전달해야 해요. 또한 Fortran 함수를 호출할 때 모든 입력은 힙이나 스택에 할당된 값의 포인터로 전달되어야 해요. 이는 보통 힙에 할당되는 배열 및 그 밖의 가변 객체뿐 아니라, 보통 스택에 할당되고 C나 줄리아 호출 규약을 쓸 때 흔히 레지스터로 전달되는 정수·부동소수점 같은 스칼라 값에도 적용돼요.
라이브러리 함수에 대한 호출을 생성하는 @ccall의 문법은 다음과 같아요.
@ccall library.function_name(argvalue1::argtype1, ...)::returntype
@ccall function_name(argvalue1::argtype1, ...)::returntype
@ccall $function_pointer(argvalue1::argtype1, ...)::returntype
여기서 library는 문자열 상수 또는 전역 변수 이름이에요 (아래의 비상수 함수 명세(Non-constant Function Specifications) 참고). 라이브러리는 이름만일 수도 있고 라이브러리의 전체 경로를 지정할 수도 있어요. 라이브러리를 생략할 수도 있는데, 그 경우 함수 이름이 현재 실행 파일, 현재 libc, 또는 libjulia(-internal)에서 해석돼요. 이 형태는 C 라이브러리 함수, 줄리아 런타임의 함수, 또는 줄리아에 연결된 애플리케이션의 함수를 호출하는 데 쓰일 수 있어요. 라이브러리 생략으로 임의의 라이브러리의 함수를 호출하는 데(예: dlsym에 RTLD_DEFAULT 지정)는 쓸 수 없어요. 그런 동작은 느리고 복잡하며 모든 플랫폼에서 구현되지 않으니까요. 대안으로 @ccall은 함수 포인터 $function_pointer — 예컨대 Libdl.dlsym이 반환하는 것 — 를 호출하는 데도 쓸 수 있어요. argtypes는 C-함수 시그니처에 대응하고 argvalues는 함수에 전달할 실제 인자 값들이에요.
참고: C 타입을 줄리아 타입으로 매핑하는 방법은 아래를 보세요.
완전하지만 간단한 예시로, 다음은 대부분의 Unix 파생 시스템에서 표준 C 라이브러리의 clock 함수를 호출해요.
julia> t = @ccall clock()::Int32
2292761
julia> typeof(t)
Int32
clock은 인자를 받지 않고 Int32를 반환해요. 환경 변수의 값에 대한 포인터를 얻는 getenv 함수를 호출하려면 이런 호출을 하면 돼요.
julia> path = @ccall getenv("SHELL"::Cstring)::Cstring
Cstring(@0x00007fff5fbffc45)
julia> unsafe_string(path)
"/bin/bash"
실전에서는, 특히 재사용 가능한 기능을 제공할 때, 보통 @ccall 사용을 인자를 설정한 다음 C나 Fortran 함수가 지정하는 방식으로 오류를 검사하는 줄리아 함수로 감싸요. 오류가 발생하면 정상적인 줄리아 예외로 던져집니다. C와 Fortran API는 오류 조건을 나타내는 방식이 악명높게 일관되지 않기 때문에 특히 중요해요. 예를 들어 C 라이브러리 함수 getenv는 env.jl의 실제 정의의 단순화 버전인 다음 줄리아 함수로 감싸져 있어요.
function getenv(var::AbstractString)
val = @ccall getenv(var::Cstring)::Cstring
if val == C_NULL
error("getenv: undefined variable: ", var)
end
return unsafe_string(val)
end
C getenv 함수는 C_NULL을 반환해 오류를 나타내지만, 다른 표준 C 함수들은 -1, 0, 1, 그리고 다른 특별 값들을 반환하는 것을 포함해 다양한 방식으로 오류를 나타내요. 이 래퍼는 호출자가 존재하지 않는 환경 변수를 얻으려 하면 문제를 나타내는 예외를 던져요.
julia> getenv("SHELL")
"/bin/bash"
julia> getenv("FOOBAR")
ERROR: getenv: undefined variable: FOOBAR
여기 로컬 머신의 호스트네임을 알아내는 약간 더 복잡한 예시가 있어요.
function gethostname()
hostname = Vector{UInt8}(undef, 256) # MAXHOSTNAMELEN
err = @ccall gethostname(hostname::Ptr{UInt8}, sizeof(hostname)::Csize_t)::Int32
Base.systemerror("gethostname", err != 0)
hostname[end] = 0 # ensure null-termination
return GC.@preserve hostname unsafe_string(pointer(hostname))
end
이 예시는 먼저 바이트 배열을 할당해요. 그런 다음 C 라이브러리 함수 gethostname을 호출해 배열을 호스트네임으로 채워요. 마지막으로 호스트네임 버퍼에 대한 포인터를 가져와 그 포인터를, null로 종결되는 C 문자열이라고 가정해 줄리아 문자열로 변환해요.
C 라이브러리가 호출자가 메모리를 할당해 피호출자에게 전달해 채우도록 요구하는 이 패턴을 쓰는 것은 흔해요. 이렇게 줄리아에서 메모리를 할당하는 것은 보통 초기화되지 않은 배열을 만들고 그 데이터에 대한 포인터를 C 함수에 전달해 이뤄져요. 그래서 여기서 Cstring 타입을 쓰지 않는 거예요. 배열이 초기화되지 않아 null 바이트를 포함할 수 있으니까요. @ccall의 일부로 Cstring으로 변환하면 포함된 null 바이트를 검사하고 변환 오류를 던질 수 있어요.
unsafe_string으로 pointer(hostname)를 역참조하는 것은 안전하지 않은(unsafe) 연산이에요. 그 사이에 가비지 컬렉션되었을 수 있는 hostname에 할당된 메모리에 접근해야 하니까요. GC.@preserve 매크로는 이를 방지해서 유효하지 않은 메모리 위치에 접근하지 않게 해 줘요.
마지막으로 경로로 라이브러리를 지정하는 예시가 있어요. 다음 내용으로 공유 라이브러리를 만듭니다.
#include <stdio.h>
void say_y(int y)
{
printf("Hello from C: got y = %d.\n", y);
}
그리고 gcc -fPIC -shared -o mylib.so mylib.c로 컴파일해요. 그런 다음 라이브러리 이름으로 (절대) 경로를 지정해 호출할 수 있어요.
julia> @ccall "./mylib.so".say_y(5::Cint)::Cvoid
Hello from C: got y = 5.
본문
C와 호환되는 줄리아 함수 포인터 만들기 (Creating C-Compatible Julia Function Pointers)
네이티브 C 함수에 함수 포인터 인자를 받는 것에 줄리아 함수를 전달할 수 있어요. 예를 들어 형태가 다음인 C 프로토타입과 일치시키려면
typedef returntype (*functiontype)(argumenttype, ...)
@cfunction 매크로가 줄리아 함수 호출에 대한 C 호환 함수 포인터를 생성해요. @cfunction의 인자는 다음과 같아요.
- 줄리아 함수
- 함수의 반환 타입
- 함수 시그니처에 대응하는 입력 타입의 튜플
참고:
@ccall과 마찬가지로 반환 타입과 입력 타입은 리터럴 상수여야 해요.
참고: 현재는 플랫폼 기본 C 호출 규약만 지원돼요. 이는
@cfunction으로 생성된 포인터를 32비트 Windows에서 WINAPI가 stdcall 함수를 기대하는 호출에는 사용할 수 없지만, WIN64에서는 사용할 수 있다는 뜻이에요 (WIN64에서 stdcall은 C 호출 규약과 통합되니까요).
참고:
@cfunction으로 노출된 콜백 함수는 오류를 던지면 안 돼요. 그렇게 하면 제어가 예상치 못하게 줄리아 런타임으로 돌아가 프로그램을 정의되지 않은 상태로 남길 수 있으니까요.
고전적인 예시는 표준 C 라이브러리의 qsort 함수로, 다음과 같이 선언돼요.
void qsort(void *base, size_t nitems, size_t size,
int (*compare)(const void*, const void*));
base 인자는 길이 nitems, 각 요소 크기 size 바이트의 배열에 대한 포인터예요. compare는 두 요소 a와 b에 대한 포인터를 받고, a가 b 앞에 나타나야 하면 0보다 작은/큰 정수를 반환하는 콜백 함수예요 (어느 순서든 허용되면 0).
이제 줄리아에 qsort 함수(줄리아 내장 sort 함수 대신)를 사용해 정렬하려는 1차원 값 배열 A가 있다고 가정해 볼게요. qsort를 호출하고 인자를 전달하는 것을 고려하기 전에 비교 함수를 작성해야 해요.
julia> function mycompare(a, b)::Cint
return (a < b) ? -1 : ((a > b) ? +1 : 0)
end;
qsort는 C int를 반환하는 비교 함수를 기대하므로 반환 타입을 Cint로 주석달아요.
이 함수를 C에 전달하려면 @cfunction 매크로로 그 주소를 얻어요.
julia> mycompare_c = @cfunction(mycompare, Cint, (Ref{Cdouble}, Ref{Cdouble}));
@cfunction은 세 인자를 요구해요. 줄리아 함수(mycompare), 반환 타입(Cint), 그리고 입력 인자 타입의 리터럴 튜플. 이 경우 Cdouble(Float64) 요소 배열을 정렬하려는 거예요.
qsort에 대한 최종 호출은 이렇게 보여요.
julia> A = [1.3, -2.7, 4.4, 3.1];
julia> @ccall qsort(A::Ptr{Cdouble}, length(A)::Csize_t, sizeof(eltype(A))::Csize_t, mycompare_c::Ptr{Cvoid})::Cvoid
julia> A
4-element Vector{Float64}:
-2.7
1.3
3.1
4.4
예시에서 보듯, 원래 줄리아 배열 A가 이제 정렬됐어요: [-2.7, 1.3, 3.1, 4.4]. 줄리아가 배열을 Ptr{Cdouble}로 변환하고, 요소 타입의 바이트 크기를 계산하는 등의 일을 처리해 주는 것에 주목하세요.
재미로, mycompare에 println("mycompare($a, $b)") 줄을 넣어 보세요. qsort가 수행하는 비교들을 볼 수 있고(그리고 정말로 당신이 전달한 줄리아 함수를 호출하는지 확인할 수 있고) 거예요.
C 타입을 줄리아에 매핑하기 (Mapping C Types to Julia)
선언된 C 타입을 그것의 줄리아 선언과 정확히 일치시키는 것이 아주 중요해요. 불일치는 한 시스템에서 올바르게 동작하던 코드가 다른 시스템에서 실패하거나 불확정적인 결과를 내게 할 수 있어요.
C 함수를 호출하는 과정의 어디에서도 C 헤더 파일이 사용되지 않는다는 점에 주의하세요. 줄리아 타입과 호출 시그니처가 C 헤더 파일의 것을 정확히 반영하도록 하는 것은 당신의 책임이에요. [2]
자동 타입 변환 (Automatic Type Conversion)
줄리아는 각 인자를 지정된 타입으로 변환하는 Base.cconvert 함수에 대한 호출을 자동으로 삽입해요. 예를 들어 다음 호출은
@ccall "libfoo".foo(x::Int32, y::Float64)::Cvoid
이렇게 작성된 것처럼 동작해요.
c_x = Base.cconvert(Int32, x)
c_y = Base.cconvert(Float64, y)
GC.@preserve c_x c_y begin
@ccall "libfoo".foo(
Base.unsafe_convert(Int32, c_x)::Int32,
Base.unsafe_convert(Float64, c_y)::Float64
)::Cvoid
end
Base.cconvert는 보통 그냥 convert를 호출하지만, C에 전달하기에 더 적절한 임의의 새 객체를 반환하도록 정의될 수 있어요. 이것은 C 코드가 접근할 메모리의 모든 할당을 수행하는 데 쓰여야 해요. 예를 들어 객체(예: 문자열)의 배열을 포인터 배열로 변환하는 데 쓰여요.
Base.unsafe_convert는 Ptr 타입으로의 변환을 처리해요. 객체를 네이티브 포인터로 변환하면 가비지 컬렉터로부터 객체를 숨겨 조기에 해제될 수 있기 때문에 안전하지 않은 것으로 간주돼요.
타입 대응 (Type Correspondences)
먼저, 관련된 줄리아 타입 용어 몇 가지를 검토해 볼게요.
| Syntax / Keyword | Example | Description |
|---|---|---|
mutable struct |
BitSet |
"구체 타입(Concrete Type)" :: 타입 태그를 포함해 관련 데이터를 묶고, 줄리아 GC가 관리하며, 객체 정체성(identity)으로 정의되는 것. 구체 타입의 타입 매개변수는 인스턴스가 만들어지려면 완전히 정의되어야 해요(TypeVar는 허용되지 않음). isconcretetype도 참고. |
abstract type |
Any, AbstractArray{T, N}, Complex{T} |
"슈퍼 타입(Super Type)" :: 인스턴스화할 수 없는 (구체 타입이 아닌) 수퍼타입이지만, 타입 그룹을 설명하는 데 쓸 수 있음. isabstracttype도 참고. |
T{A} |
Vector{Int} |
"타입 매개변수(Type Parameter)" :: 타입의 특수화 (보통 디스패치나 저장 최적화에 쓰임). |
"TypeVar" :: 타입 매개변수 선언의 T는 TypeVar(타입 변수의 줄임말)이라고 불러요. |
||
primitive type |
Int, Float64 |
"원시 타입(Primitive Type)" :: 필드는 없지만 크기는 있는 타입. 값으로 저장되고 값으로 정의됨. |
struct |
Pair{Int, Int} |
"Struct" :: 모든 필드가 상수로 정의된 타입. 값으로 정의되고, 타입 태그와 함께 저장될 수 있음. |
ComplexF64 (isbits) |
"Is-Bits" :: 원시 타입, 또는 모든 필드가 다른 isbits 타입인 struct 타입. 값으로 정의되고 타입 태그 없이 저장됨. | |
struct ...; end |
nothing |
"싱글턴(Singleton)" :: 필드가 없는 구체 타입 또는 Struct. |
(...) 또는 tuple(...) |
(1, 2, 3) |
"튜플(Tuple)" :: 익명 struct 타입이나 상수 배열과 유사한 불변 데이터 구조. 배열 또는 struct로 표현됨. |
Bits 타입 (Bits Types)
다른 어떤 타입도 같은 방식으로 동작하도록 정의될 수 없으므로 알아둬야 할 몇 가지 특별한 타입이 있어요.
-
Float32
C의
float타입(또는 Fortran의REAL*4)과 정확히 대응. -
Float64
C의
double타입(또는 Fortran의REAL*8)과 정확히 대응. -
ComplexF32
C의
complex float타입(또는 Fortran의COMPLEX*8)과 정확히 대응. -
ComplexF64
C의
complex double타입(또는 Fortran의COMPLEX*16)과 정확히 대응. -
Signed
C의
signed타입 주석(또는 Fortran의 임의의INTEGER타입)과 정확히 대응.Signed의 서브타입이 아닌 모든 줄리아 타입은 부호 없음으로 가정돼요. -
Ref{T}
줄리아 GC를 통해 메모리를 관리할 수 있는
Ptr{T}처럼 동작. -
Array{T,N}
배열이
Ptr{T}인자로 C에 전달되면 reinterpret-cast되지 않아요. 줄리아는 배열의 요소 타입이T와 일치할 것을 요구하고, 첫 번째 요소의 주소가 전달돼요.따라서
Array가 잘못된 형식의 데이터를 담고 있으면trunc.(Int32, A)같은 호출로 명시적으로 변환해야 해요.데이터를 미리 변환하지 않고 다른 타입의 포인터로 배열
A를 전달하려면 (예를 들어 해석되지 않은 바이트에 대해 동작하는 함수에Float64배열을 전달하려면) 인자를Ptr{Cvoid}로 선언할 수 있어요.eltype
Ptr{T}의 배열이Ptr{Ptr{T}}인자로 전달되면,Base.cconvert는 먼저 각 요소를 그것의Base.cconvert버전으로 대체한 null 종결 배열의 복사본을 만들려 해요. 예를 들어Vector{String}타입의 argv 포인터 배열을Ptr{Ptr{Cchar}}타입의 인자에 전달할 수 있게 해 줘요.
우리가 현재 지원하는 모든 시스템에서 기본 C/C++ 값 타입은 다음과 같이 줄리아 타입으로 번역될 수 있어요. 모든 C 타입에는 그 이름 앞에 C가 붙은 같은 이름의 대응하는 줄리아 타입도 있어요. 이것은 이식 가능한 코드를 쓸 때 도움이 돼요 (그리고 C의 int가 줄리아의 Int와 같지 않다는 것을 기억하는 데도).
시스템 독립 타입 (System Independent Types)
| C name | Fortran name | Standard Julia Alias | Julia Base Type |
|---|---|---|---|
unsigned char |
CHARACTER |
Cuchar |
UInt8 |
bool (_Bool in C99+) |
Cuchar |
UInt8 |
|
short |
INTEGER*2, LOGICAL*2 |
Cshort |
Int16 |
unsigned short |
Cushort |
UInt16 |
|
int, BOOL (C, typical) |
INTEGER*4, LOGICAL*4 |
Cint |
Int32 |
unsigned int |
Cuint |
UInt32 |
|
long long |
INTEGER*8, LOGICAL*8 |
Clonglong |
Int64 |
unsigned long long |
Culonglong |
UInt64 |
|
intmax_t |
Cintmax_t |
Int64 |
|
uintmax_t |
Cuintmax_t |
UInt64 |
|
float |
REAL*4 |
Cfloat |
Float32 |
double |
REAL*8 |
Cdouble |
Float64 |
complex float |
COMPLEX*8 |
ComplexF32 |
Complex{Float32} |
complex double |
COMPLEX*16 |
ComplexF64 |
Complex{Float64} |
ptrdiff_t |
Cptrdiff_t |
Int |
|
ssize_t |
Cssize_t |
Int |
|
size_t |
Csize_t |
UInt |
|
void |
Cvoid |
||
void and [[noreturn]] or _Noreturn |
Union{} |
||
void* |
Ptr{Cvoid} (또는 그와 유사하게 Ref{Cvoid}) |
||
T* (여기서 T는 적절히 정의된 타입을 나타냄) |
Ref{T} (T가 isbits 타입인 경우에만 안전하게 변형될 수 있음) |
||
char* (또는 char[], 예: 문자열) |
CHARACTER*N |
null 종결이면 Cstring, 아니면 Ptr{UInt8} |
|
char** (또는 *char[]) |
Ptr{Ptr{UInt8}} |
||
jl_value_t* (any Julia Type) |
Any |
||
jl_value_t* const* (a reference to a Julia value) |
Ref{Any} (const, 변형은 write barrier가 필요하고 이를 올바르게 삽입할 수 없으므로) |
||
va_arg |
지원되지 않음 | ||
... (variadic function specification) |
T... (여기서 T는 위 타입 중 하나, ccall 함수를 쓸 때) |
||
... (variadic function specification) |
; va_arg1::T, va_arg2::S, etc. (@ccall 매크로로만 지원) |
Cstring 타입은 본질적으로 Ptr{UInt8}의 동의어인데, 줄리아 문자열이 임베디드 null 문자를 포함하면 Cstring으로의 변환이 오류를 던진다는 점만 달라요 (C 루틴이 null을 종결자로 취급하면 문자열이 조용히 잘릴 수 있으니까요). null 종결을 가정하지 않는 C 루틴에 char*를 전달하거나(예: 명시적 문자열 길이를 전달하므로), 줄리아 문자열에 null이 확실히 없다고 알고 검사를 건너뛰고 싶다면, 인자 타입으로 Ptr{UInt8}을 쓸 수 있어요. Cstring은 ccall의 반환 타입으로도 쓸 수 있는데, 그 경우 당연히 추가 검사를 도입하지 않고 호출의 가독성만 높이기 위한 것이에요.
시스템 의존 타입 (System Dependent Types)
| C name | Standard Julia Alias | Julia Base Type |
|---|---|---|
char |
Cchar |
Int8 (x86, x86_64), UInt8 (powerpc, arm) |
long |
Clong |
Int (UNIX), Int32 (Windows) |
unsigned long |
Culong |
UInt (UNIX), UInt32 (Windows) |
wchar_t |
Cwchar_t |
Int32 (UNIX), UInt16 (Windows) |
참고: Fortran을 호출할 때 모든 입력은 힙이나 스택에 할당된 값의 포인터로 전달되어야 하므로, 위의 모든 타입 대응은 그 타입 명세 주위에 추가
Ptr{..}또는Ref{..}래퍼를 포함해야 해요.
경고: 문자열 인자(
char*)의 줄리아 타입은Cstring(null 종결 데이터가 기대되면)이거나, 그렇지 않으면Ptr{Cchar}또는Ptr{UInt8}(이 두 포인터 타입은 같은 효과)이어야 해요. 위에서 설명한 대로String이 아니에요. 마찬가지로 배열 인자(T[]또는T*)의 줄리아 타입도Vector{T}가 아니라Ptr{T}여야 해요.
경고: 줄리아의
Char타입은 32비트로, 모든 플랫폼의 wide-character 타입(wchar_t또는wint_t)과 같지 않아요.
경고:
Union{}반환 타입은 함수가 반환하지 않는다는 뜻이에요. 즉 C++11[[noreturn]]또는 C11_Noreturn(예:jl_throw또는longjmp)이에요. 값을 반환하지 않는(무효,void) 하지만 반환은 하는 함수에는 쓰지 마세요. 그런 경우Cvoid를 쓰세요.
참고:
wchar_t*인자의 줄리아 타입은Cwstring(C 루틴이 null 종결 문자열을 기대하면)이거나, 그렇지 않으면Ptr{Cwchar_t}이에요. 또한 줄리아의 UTF-8 문자열 데이터는 내부적으로 null 종결되므로, null 종결 데이터를 기대하는 C 함수에 복사 없이 전달될 수 있어요 (단Cwstring타입을 쓰면 문자열 자체가 null 문자를 포함할 때 오류가 던져집니다).
참고:
char**타입의 인자를 받는 C 함수는 줄리아에서Ptr{Ptr{UInt8}}타입을 써서 호출할 수 있어요. 예를 들어 다음과 같은 형태의 C 함수는
int main(int argc, char **argv);
다음 줄리아 코드로 호출할 수 있어요.
argv = [ "a.out", "arg1", "arg2" ]
@ccall main(length(argv)::Int32, argv::Ptr{Ptr{UInt8}})::Int32
참고: 가변 길이 문자열
character(len=*)타입을 받는 Fortran 함수는 문자열 길이가 숨겨진 인자(hidden arguments)로 제공돼요. 인자 목록에서 이 인자들의 타입과 위치는 컴파일러별로 다르며, 컴파일러 벤더는 보통 타입으로Csize_t를 기본값으로 쓰고 숨겨진 인자를 인자 목록 끝에 덧붙여요. 이 동작은 일부 컴파일러(GNU)에서 고정되어 있지만, 다른 컴파일러(Intel, PGI)는 숨겨진 인자를 문자 인자 바로 뒤에 두는 것을 선택적으로 허용해요. 예를 들어 형태가 다음인 Fortran 서브루틴은
subroutine test(str1, str2)
character(len=*) :: str1,str2
길이를 덧붙인 다음 줄리아 코드로 호출할 수 있어요.
str1 = "foo"
str2 = "bar"
ccall(:test, Cvoid, (Ptr{UInt8}, Ptr{UInt8}, Csize_t, Csize_t),
str1, str2, sizeof(str1), sizeof(str2))
경고: Fortran 컴파일러는 포인터, assumed-shape(
:) 및 assumed-size(*) 배열에 대한 다른 숨겨진 인자도 추가할 수 있어요. 이런 동작은ISO_C_BINDING을 사용하고 서브루틴 정의에bind(c)를 포함하면 피할 수 있으며, 상호 운용 가능한 코드에는 이를 강력히 권장해요. 이 경우 몇 가지 언어 기능(예:character(len=1)만 문자열을 전달할 수 있음)을 희생하는 대신 숨겨진 인자가 없어요.
참고:
Cvoid를 반환하도록 선언된 C 함수는 줄리아에서 값nothing을 반환해요.
구조체 타입 대응 (Struct Type Correspondences)
C의 struct나 Fortran90의 TYPE(또는 F77의 일부 변형에서 STRUCTURE / RECORD) 같은 복합 타입은, 같은 필드 레이아웃을 가진 struct 정의를 만들어 줄리아에 반영할 수 있어요.
재귀적으로 사용될 때 isbits 타입은 인라인으로 저장돼요. 다른 모든 타입은 데이터에 대한 포인터로 저장돼요. C에서 다른 struct 안에서 by-value로 사용되는 struct를 반영할 때, 필드를 수동으로 복사하려 시도하지 않는 것이 필수적이에요. 올바른 필드 정렬을 보존하지 못하니까요. 대신 isbits struct 타입을 선언하고 그것을 쓰세요. 이름 없는 struct는 줄리아로의 번역에서 불가능해요.
Packed struct와 union 선언은 줄리아가 지원하지 않아요.
사전에 가장 큰 크기를 가질 필드(padding 포함 가능)를 안다면 union의 근사치를 얻을 수 있어요. 필드를 줄리아로 번역할 때 줄리아 필드를 그 타입만으로 선언하세요.
매개변수의 배열은 NTuple로 표현할 수 있어요. 예를 들어 C 표기법의 다음 struct는
struct B {
int A[3];
};
b_a_2 = B.A[2];
줄리아로 이렇게 쓸 수 있어요.
struct B
A::NTuple{3, Cint}
end
b_a_2 = B.A[3] # note the difference in indexing (1-based in Julia, 0-based in C)
알 수 없는 크기의 배열([] 또는 [0]으로 지정된 C99 호환 가변 길이 struct)은 직접 지원되지 않아요. 이를 다루는 가장 좋은 방법은 바이트 오프셋을 직접 다루는 것이에요. 예를 들어 C 라이브러리가 적절한 문자열 타입을 선언하고 그것에 대한 포인터를 반환했다면:
struct String {
int strlen;
char data[];
};
줄리아에서 부분들을 독립적으로 접근해 그 문자열의 복사본을 만들 수 있어요.
str = from_c::Ptr{Cvoid}
len = unsafe_load(Ptr{Cint}(str))
unsafe_string(str + Core.sizeof(Cint), len)
타입 매개변수 (Type Parameters)
@ccall과 @cfunction의 타입 인자는 그 사용을 포함하는 메서드가 정의될 때 정적으로 평가돼요. 따라서 리터럴 튜플의 형태를 취해야 하며, 변수가 될 수 없고 지역 변수를 참조할 수 없어요.
이상한 제약처럼 들릴 수 있지만, C는 줄리아 같은 동적 언어가 아니므로 함수는 정적으로 알려진 고정 시그니처를 가진 인자 타입만 받을 수 있다는 것을 기억하세요.
하지만 의도한 C ABI를 계산하려면 타입 레이아웃이 정적으로 알려져야 하지만, 함수의 정적 매개변수는 이 정적 환경의 일부로 간주돼요. 함수의 정적 매개변수는 타입의 레이아웃에 영향을 주지 않는 한 호출 시그니처의 타입 매개변수로 사용될 수 있어요. 예를 들어 f(x::T) where {T} = @ccall valid(x::Ptr{T})::Ptr{T}는 유효해요. Ptr는 항상 word-size 원시 타입이니까요. 하지만 g(x::T) where {T} = @ccall notvalid(x::T)::T는 유효하지 않아요. T의 타입 레이아웃이 정적으로 알려져 있지 않으니까요.
SIMD 값 (SIMD Values)
C/C++ 루틴의 인자나 반환 값이 네이티브 SIMD 타입이면, 대응하는 줄리아 타입은 SIMD 타입에 자연스럽게 매핑되는 VecElement의 균질 튜플이에요. 구체적으로:
- 튜플은 SIMD 타입과 같은 크기와 요소여야 해요. 예를 들어 x86에서
__m128을 나타내는 튜플은 크기 16바이트이고Float32요소여야 해요. - 튜플의 요소 타입은 바이트 수가 2의 거듭제곱(예: 1, 2, 4, 8, 16 등 (
Int8이나Float64같은))인 원시 타입인VecElement{T}의 인스턴스여야 해요.
예를 들어 AVX 내장 함수를 사용하는 이 C 루틴을 생각해 보세요.
#include <immintrin.h>
__m256 dist( __m256 a, __m256 b ) {
return _mm256_sqrt_ps(_mm256_add_ps(_mm256_mul_ps(a, a),
_mm256_mul_ps(b, b)));
}
다음 줄리아 코드는 ccall로 dist를 호출해요.
const m256 = NTuple{8, VecElement{Float32}}
a = m256(ntuple(i -> VecElement(sin(Float32(i))), 8))
b = m256(ntuple(i -> VecElement(cos(Float32(i))), 8))
function call_dist(a::m256, b::m256)
@ccall "libdist".dist(a::m256, b::m256)::m256
end
println(call_dist(a,b))
호스트 머신은 필수 SIMD 레지스터를 가져야 해요. 예를 들어 위 코드는 AVX를 지원하지 않는 호스트에서는 동작하지 않아요.
메모리 소유권 (Memory Ownership)
malloc/free
이런 객체의 메모리 할당·해제는 어떤 C 프로그램에서처럼, 사용 중인 라이브러리의 적절한 정리 루틴 호출로 처리해야 해요. C 라이브러리에서 받은 객체를 줄리아에서 Libc.free로 해제하려 하지 마세요. wrong 라이브러리를 통해 free 함수가 호출되어 프로세스가 중단될 수 있으니까요. 반대(줄리아에서 할당된 객체를 외부 라이브러리가 해제하도록 전달)도 마찬가지로 유효하지 않아요.
T, Ptr{T}, Ref{T}를 언제 쓸까 (When to use T, Ptr{T} and Ref{T})
외부 C 루틴 호출을 감싸는 줄리아 코드에서, 일반(비포인터) 데이터는 값으로 전달되므로 @ccall 안에서 타입 T로 선언돼야 해요. 포인터를 받는 C 코드의 경우, 입력 인자의 타입에는 일반적으로 Ref{T}를 써서 Base.cconvert의 암시적 호출을 통해 줄리아나 C 어느 쪽이든 관리하는 메모리에 대한 포인터를 사용할 수 있게 해요. 반면 호출된 C 함수가 반환하는 포인터는 출력 타입 Ptr{T}로 선언돼야 해요. 가리키는 메모리가 C만 관리한다는 것을 반영해서요. C struct에 포함된 포인터는 대응하는 줄리아 struct 타입 안에서 해당 C struct의 내부 구조를 모방하도록 설계된 Ptr{T} 타입의 필드로 표현되어야 해요.
외부 Fortran 루틴 호출을 감싸는 줄리아 코드에서, Fortran은 모든 변수를 메모리 위치에 대한 포인터로 전달하므로 모든 입력 인자를 타입 Ref{T}로 선언해야 해요. 반환 타입은 Fortran 서브루틴의 경우 Cvoid이거나, 타입 T를 반환하는 Fortran 함수의 경우 T여야 해요.
C 함수를 줄리아에 매핑하기 (Mapping C Functions to Julia)
@ccall / @cfunction 인자 번역 가이드
C 인자 목록을 줄리아로 번역하려면:
T(여기서 T는 원시 타입 중 하나:char,int,long,short,float,double,complex,enum또는 이들의 typedef 동등형)T(여기서 T는 위 표에 따라 동등한 줄리아 Bits 타입)T가 enum이면 인자 타입은Cint또는Cuint와 동등해야 함- 인자 값은 복사됨(값으로 전달)
struct T(struct에 대한 typedef 포함)T(여기서 T는 구체 줄리아 타입)- 인자 값은 복사됨(값으로 전달)
vector T(또는__attribute__ vector_size, 또는__m128같은 typedef)NTuple{N, VecElement{T}}(여기서 T는 올바른 크기의 원시 줄리아 타입, N은 벡터의 요소 수이며vector_size / sizeof T와 같음)
void*- 이 매개변수가 어떻게 사용되는지에 따라 다름. 먼저 의도한 포인터 타입으로 번역한 다음, 이 목록의 나머지 규칙으로 줄리아 동등형을 결정
- 이 인자가 정말로 알 수 없는 포인터라면
Ptr{Cvoid}로 선언될 수 있음
jl_value_t*Any- 인자 값은 유효한 줄리아 객체여야 함
jl_value_t* const*Ref{Any}- 인자 목록은 유효한 줄리아 객체(또는
C_NULL)여야 함 - 객체가 GC로 보존되도록 별도로 마련할 수 없는 한 출력 매개변수에 쓸 수 없음
T*Ref{T}(여기서 T는 T에 대응하는 줄리아 타입)- 인자가 inlinealloc 타입이면(그렇지 않으면 isbits 포함) 인자 값이 복사됨. 값은 유효한 줄리아 객체여야 함
T (*)(...)(예: 함수에 대한 포인터)Ptr{Cvoid}(이 포인터를 만들려면@cfunction을 명시적으로 써야 할 수 있음)
...(예: 가변 인자, vararg)- [@ccall용]:
; va_arg1::T, va_arg2::S, etc(여기서 T와 S는 줄리아 타입. 즉;로 일반 인자와 varargs를 구분) - 현재
@cfunction에서는 지원되지 않음
- [@ccall용]:
va_argccall이나@cfunction에서 지원되지 않음
@ccall / @cfunction 반환 타입 번역 가이드
C 반환 타입을 줄리아로 번역하려면:
voidCvoid(이것은 싱글턴 인스턴스nothing::Cvoid를 반환함)
T(여기서 T는 원시 타입 중 하나:char,int,long,short,float,double,complex,enum또는 이들의 typedef 동등형)- C 인자 목록과 같음
- 인자 값은 복사됨(값으로 반환)
struct T(struct에 대한 typedef 포함)- C 인자 목록과 같음
- 인자 값은 복사됨(값으로 반환)
vector T- C 인자 목록과 같음
void*- 이 매개변수가 어떻게 사용되는지에 따라 다름. 먼저 의도한 포인터 타입으로 번역한 다음, 이 목록의 나머지 규칙으로 줄리아 동등형을 결정
- 이 인자가 정말로 알 수 없는 포인터라면
Ptr{Cvoid}로 선언될 수 있음
jl_value_t*Any- 인자 값은 유효한 줄리아 객체여야 함
jl_value_t**Ptr{Any}(Ref{Any}는 반환 타입으로 유효하지 않음)
T*- 메모리가 이미 줄리아가 소유하거나 isbits 타입이고 non-null인 것으로 알려져 있으면:
Ref{T}(여기서 T는 T에 대응하는 줄리아 타입)- 반환 타입
Ref{Any}는 유효하지 않음.Any(jl_value_t*에 대응) 또는Ptr{Any}(jl_value_t**에 대응) 중 하나여야 함 - T가 isbits 타입이면 C는
Ref{T}로 반환된 메모리를 수정해서는 안 됨
- 메모리가 C가 소유하면:
Ptr{T}(여기서 T는 T에 대응하는 줄리아 타입)
- 메모리가 이미 줄리아가 소유하거나 isbits 타입이고 non-null인 것으로 알려져 있으면:
T (*)(...)(예: 함수에 대한 포인터)Ptr{Cvoid}. 줄리아에서 직접 호출하려면 이것을@ccall의 첫 번째 인자로 전달해야 함. Indirect Calls 참고.
입력 수정을 위한 포인터 전달 (Passing Pointers for Modifying Inputs)
C는 여러 반환 값을 지원하지 않으므로, 종종 C 함수는 함수가 수정할 데이터에 대한 포인터를 받아요. @ccall 안에서 이를 수행하려면 먼저 값을 적절한 타입의 Ref{T} 안에 캡슐화해야 해요. 이 Ref 객체를 인자로 전달하면 줄리아가 캡슐화된 데이터에 대한 C 포인터를 자동으로 전달해요.
width = Ref{Cint}(0)
range = Ref{Cfloat}(0)
@ccall foo(width::Ref{Cint}, range::Ref{Cfloat})::Cvoid
반환 시 width와 range의 내용은(만약 foo가 변경했다면) width[]와 range[]로 검색할 수 있어요. 즉 0차원 배열처럼 동작해요.
C 래퍼 예시 (C Wrapper Examples)
Ptr 타입을 반환하는 C 래퍼의 간단한 예시부터 시작할게요.
mutable struct gsl_permutation
end
# The corresponding C signature is
# gsl_permutation * gsl_permutation_alloc (size_t n);
function permutation_alloc(n::Integer)
output_ptr = @ccall "libgsl".gsl_permutation_alloc(n::Csize_t)::Ptr{gsl_permutation}
if output_ptr == C_NULL # Could not allocate memory
throw(OutOfMemoryError())
end
return output_ptr
end
GNU 과학 라이브러리(여기서는 :libgsl로 접근 가능하다고 가정)는 불투명 포인터 gsl_permutation *를 C 함수 gsl_permutation_alloc의 반환 타입으로 정의해요. 사용자 코드는 gsl_permutation struct 내부를 볼 필요가 없으므로, 대응하는 줄리아 래퍼는 내부 필드가 없고 그 유일한 목적이 Ptr 타입의 타입 매개변수에 놓이는 새 타입 선언 gsl_permutation만 필요해요. ccall의 반환 타입은 Ptr{gsl_permutation}으로 선언되는데, output_ptr이 할당하고 가리키는 메모리는 C가 제어하니까요.
입력 n은 값으로 전달되므로 함수의 입력 시그니처는 Ref나 Ptr 없이 그냥 ::Csize_t로 선언돼요. (래퍼가 대신 Fortran 함수를 호출했다면, Fortran 변수는 포인터로 전달되므로 대응하는 함수 입력 시그니처는 ::Ref{Csize_t}가 되었을 거예요.) 게다가 n은 Csize_t 정수로 변환 가능한 어떤 타입이든 될 수 있어요. ccall이 Base.cconvert(Csize_t, n)을 암시적으로 호출하니까요.
여기 대응하는 소멸자(destructor)를 감싸는 두 번째 예시가 있어요.
# The corresponding C signature is
# void gsl_permutation_free (gsl_permutation * p);
function permutation_free(p::Ptr{gsl_permutation})
@ccall "libgsl".gsl_permutation_free(p::Ptr{gsl_permutation})::Cvoid
end
여기 줄리아 배열을 전달하는 세 번째 예시가 있어요.
# The corresponding C signature is
# int gsl_sf_bessel_Jn_array (int nmin, int nmax, double x,
# double result_array[])
function sf_bessel_Jn_array(nmin::Integer, nmax::Integer, x::Real)
if nmax < nmin
throw(DomainError())
end
result_array = Vector{Cdouble}(undef, nmax - nmin + 1)
errorcode = @ccall "libgsl".gsl_sf_bessel_Jn_array(
nmin::Cint, nmax::Cint, x::Cdouble, result_array::Ref{Cdouble})::Cint
if errorcode != 0
error("GSL error code $errorcode")
end
return result_array
end
감싸진 C 함수는 정수 오류 코드를 반환하고, Bessel J 함수의 실제 평가 결과는 줄리아 배열 result_array를 채워요. 이 변수는 메모리가 줄리아가 할당·관리하므로 Ref{Cdouble}로 선언돼요. Base.cconvert(Ref{Cdouble}, result_array)의 암시적 호출은 줄리아 배열 데이터 구조에 대한 줄리아 포인터를 C가 이해하는 형태로 풉니다.
Fortran 래퍼 예시 (Fortran Wrapper Example)
다음 예시는 일반적인 Fortran 라이브러리(libBLAS)의 함수를 호출해 내적(dot product)을 계산하기 위해 ccall을 활용해요. 인자 매핑이 위와 조금 다르다는 점에 주목하세요. 줄리아에서 Fortran으로 매핑해야 하니까요. 모든 인자 타입에 Ref 또는 Ptr를 지정해요. 이 맹글링 관례는 당신의 Fortran 컴파일러와 운영 체제에 특화될 수 있고 문서화되지 않았을 가능성이 높아요. 하지만 각각을 Ref(또는 동등한 곳에서 Ptr)로 감싸는 것은 Fortran 컴파일러 구현의 빈번한 요구사항이에요.
function compute_dot(DX::Vector{Float64}, DY::Vector{Float64})
@assert length(DX) == length(DY)
n = length(DX)
incx = incy = 1
product = @ccall "libLAPACK".ddot(
n::Ref{Int32}, DX::Ptr{Float64}, incx::Ref{Int32}, DY::Ptr{Float64}, incy::Ref{Int32})::Float64
return product
end
가비지 컬렉션 안전성 (Garbage Collection Safety)
@ccall에 데이터를 전달할 때는 pointer 함수를 사용하는 것을 피하는 것이 좋아요. 대신 Base.cconvert 메서드를 정의하고 변수를 @ccall에 직접 전달해요. @ccall은 호출이 반환될 때까지 모든 인자가 가비지 컬렉션으로부터 보존되도록 자동으로 정리해요. C API가 줄리아가 할당한 메모리에 대한 참조를 @ccall 반환 후에도 저장한다면, 객체가 가비지 컬렉터에 계속 보이도록 해야 해요. 권장 방식은 Vector{Ref} 타입의 전역 변수를 만들어 C 라이브러리가 사용을 마쳤다고 알릴 때까지 이 값들을 담아 두는 거예요.
줄리아 데이터에 대한 포인터를 만들 때마다 그 포인터를 사용을 마칠 때까지 원본 데이터가 존재하도록 해야 해요. unsafe_load와 String 같은 줄리아의 많은 메서드는 버퍼의 소유권을 가지는 대신 데이터의 복사본을 만들어, 줄리아에 영향을 주지 않고 원본 데이터를 해제(또는 변경)하는 것이 안전해요. 주목할 예외는 unsafe_wrap인데, 성능상의 이유로 밑에 있는 버퍼를 공유(또는 소유권을 가지라고 지시될 수 있음)해요.
가비지 컬렉터는 파이널라이제이션(finalization)의 어떤 순서도 보장하지 않아요. 즉 a가 b에 대한 참조를 담고 있고 a와 b 모두 가비지 컬렉션 대상이면, b가 a 이후에 파이널라이즈된다는 보장이 없어요. a의 적절한 파이널라이제이션이 b의 유효성에 달려 있다면, 다른 방식으로 처리해야 해요.
비상수 함수 명세 (Non-constant Function Specifications)
어떤 경우에는 필요한 라이브러리의 정확한 이름이나 경로를 미리 알 수 없어 런타임에 계산해야 해요. 이런 경우를 처리하려면 라이브러리 구성 요소 명세가 Libdl.LazyLibrary 같은 값일 수 있어요. 런타임이 ccall로 처음 사용될 때 그 객체에 대해 Libdl.dlopen을 호출해요.
LazyLoading을 위한 LazyLibrary 사용 (Using LazyLibrary for Lazy Loading)
Libdl.LazyLibrary는 첫 사용까지 라이브러리 로딩을 연기하는 스레드 안전 메커니즘을 제공해요. 현대 줄리아 코드에서 라이브러리 초기화의 권장 접근 방식이에요.
LazyLibrary는 ccall(), @ccall, dlopen(), dlsym(), dlpath(), 또는 cglobal()에서 처음 사용될 때 자신(과 그 의존성)을 자동으로 여는 라이브러리를 나타내요. 라이브러리는 스레드 안전한 방식으로 정확히 한 번 로드되고, 이후 호출은 로드된 라이브러리 핸들을 재사용해요.
기본 사용법 (Basic Usage)
using Libdl
# Define a LazyLibrary as a const for optimal performance
const libz = LazyLibrary("libz")
# Use directly in @ccall - library loads automatically on first call
@ccall libz.deflate(strm::Ptr{Cvoid}, flush::Cint)::Cint
# Also works with ccall
ccall((:inflate, libz), Cint, (Ptr{Cvoid}, Cint), strm, flush)
플랫폼별 라이브러리 (Platform-Specific Libraries)
여러 플랫폼에 걸쳐 동작해야 하는 코드의 경우:
const mylib = LazyLibrary(
if Sys.iswindows()
"mylib.dll"
elseif Sys.isapple()
"libmylib.dylib"
else
"libmylib.so"
end
)
의존성이 있는 라이브러리 (Libraries with Dependencies)
라이브러리가 다른 라이브러리에 의존할 때, 의존성을 지정해 올바른 순서로 로드되게 하세요.
const libfoo = LazyLibrary("libfoo")
const libbar = LazyLibrary("libbar"; dependencies=[libfoo])
# When libbar is first used, libfoo is loaded first automatically
@ccall libbar.bar_function(x::Cint)::Cint
지연 경로 구성 (Lazy Path Construction)
경로가 런타임에 결정되는 라이브러리의 경우 LazyLibraryPath를 사용하세요.
# Path is constructed when library is first accessed
const mylib = LazyLibrary(LazyLibraryPath(artifact_dir, "lib", "libmylib.so"))
초기화 콜백 (Initialization Callbacks)
라이브러리가 로드 후 초기화가 필요하면:
const mylib = LazyLibrary("libmylib";
on_load_callback = () -> @ccall mylib.initialize()::Cvoid
)
경고:
on_load_callback은 최소해야 하며 어떤 task에서도wait()를 호출하면 안 돼요. 그것은 라이브러리를 로드하는 스레드에 의해 정확히 한 번 호출돼요.
init() 패턴에서의 변환 (Conversion from init() Pattern)
LazyLibrary 이전에는 라이브러리 경로가 종종 __init__() 함수에서 계산되었어요. 이 패턴은 더 나은 성능과 스레드 안전성을 위해 LazyLibrary로 대체할 수 있어요.
__init__()를 사용한 옛 패턴:
# Old: Library path computed in __init__()
libmylib_path = ""
function __init__(
# Loads library on startup, whether it is used or not
global libmylib_path = find_library(["libmylib"])
end
function myfunc(x)
ccall((:cfunc, libmylib_path), Cint, (Cint,), x)
end
LazyLibrary를 사용한 새 패턴:
# New: Library as const, no __init__() needed
const libmylib = LazyLibrary("libmylib")
function myfunc(x)
# Library loads automatically just before calling `cfunc`
@ccall libmylib.cfunc(x::Cint)::Cint
end
자세한 내용은 Libdl.LazyLibrary 문서를 보세요.
커스텀 타입을 위한 dlopen 오버로딩 (Overloading dlopen for Custom Types)
@ccall이 실행될 때 런타임은 dlsym(:function, dlopen(library)::Ptr{Cvoid})를 호출해요. Libdl.dlopen 함수는 커스텀 타입에 대해 오버로드되어 대체 동작을 제공할 수 있어요. 하지만 일단 결정되면 라이브러리 위치와 핸들은 변하지 않는다고 가정하므로, 호출의 결과는 캐시되고 재사용될 수 있어요. 따라서 dlopen 표현식이 실행되는 횟수는 지정되지 않으며, 여러 호출에 다른 값을 반환하면 지정되지 않은(그러나 유효한) 동작이 발생해요.
계산된 함수 이름 (Computed Function Names)
더 많은 유연성이 필요하다면, eval을 통해 단계화(staging)해서 계산된 값을 함수 이름으로 사용할 수 있어요.
@eval @ccall "lib".$(string("a", "b"))()::Cint
이 표현식은 string으로 이름을 구성한 다음, 이 이름을 새 @ccall 표현식에 대입하고 그것을 평가해요. eval은 최상위에서만 동작한다는 것을 명심하세요. 그래서 이 표현식 안에서 지역 변수는 사용할 수 없어요 (값이 $로 대입되지 않는 한). 이런 이유로 eval은 보통 최상위 정의를 만들 때만 쓰는데, 예를 들어 유사한 함수를 많이 담은 라이브러리를 감쌀 때요.
간접 호출 (Indirect Calls)
@ccall의 첫 번째 인자는 사용될 때마다 런타임에 평가될 표현식일 수도 있어요. 이 경우 표현식은 호출할 네이티브 함수의 주소로 쓰일 Ptr로 평가되어야 해요. 이 동작은 첫 번째 @ccall 인자가 $로 표시되거나 첫 번째 ccall 인자가 단순한 상수 리터럴 또는 () 안의 표현식이 아닐 때 발생해요. 인자는 어떤 표현식이든 될 수 있고 지역 변수와 인자를 사용할 수 있으며 매번 다른 값을 반환할 수 있어요.
예를 들어 cglobal과 유사한 매크로를 구현할 수 있는데, dlsym으로 함수를 찾은 다음 포인터를 공유 참조에 캐시할 수 있어요 (사전 컴파일 저장 중에 C_NULL로 자동 리셋됨). 예를 들어:
macro dlsym(lib, func)
z = Ref(C_NULL)
quote
local zlocal = $z[]
if zlocal == C_NULL
zlocal = dlsym($(esc(lib))::Ptr{Cvoid}, $(esc(func)))::Ptr{Cvoid}
$z[] = zlocal
end
zlocal
end
end
const mylibvar = LazyLibrary("mylib")
@ccall $(@dlsym(dlopen(mylibvar), "myfunc"))()::Cvoid
클로저 cfunction (Closure cfunctions)
@cfunction의 첫 번째 인자는 $로 표시될 수 있는데, 그 경우 반환 값은 대신 인자를 클로저로 감싸는 struct CFunction이 돼요. 이 반환 객체를 그것의 모든 사용이 끝날 때까지 살아 있게 해야 해요. 이 참조가 버려지고 atexit에서 cfunction 포인터의 내용과 코드가 파이널라이저를 통해 지워져요. 이것은 보통 필요하지 않아요. 이 기능은 C에 없으니까요. 하지만 별도의 클로저 환경 매개변수를 제공하지 않는 설계가 나쁜 API를 다룰 때 유용할 수 있어요.
function qsort(a::Vector{T}, cmp) where T
isbits(T) || throw(ArgumentError("this method can only qsort isbits arrays"))
callback = @cfunction $cmp Cint (Ref{T}, Ref{T})
# Here, `callback` isa Base.CFunction, which will be converted to Ptr{Cvoid}
# (and protected against finalization) by the ccall
@ccall qsort(a::Ptr{T}, length(a)::Csize_t, Base.elsize(a)::Csize_t, callback::Ptr{Cvoid})
# We could instead use:
# GC.@preserve callback begin
# use(Base.unsafe_convert(Ptr{Cvoid}, callback))
# end
# if we needed to use it outside of a `ccall`
return a
end
참고: 클로저
@cfunction은 모든 플랫폼에서 사용할 수 없는 LLVM 트램펄린에 의존해요 (예: ARM과 PowerPC).
라이브러리 닫기 (Closing a Library)
라이브러리를 다시 로드할 수 있도록 닫는(언로드하는) 것이 때로 유용해요. 예를 들어 줄리아와 함께 사용할 C 코드를 개발할 때, 컴파일하고 줄리아에서 C 코드를 호출한 다음 라이브러리를 닫고 수정하고 재컴파일하고 새 변경 사항을 로드해야 할 수 있어요. 줄리아를 재시작하거나 Libdl 함수를 사용해 라이브러리를 명시적으로 관리할 수 있어요. 예를 들어:
lib = Libdl.dlopen("./my_lib.so") # Open the library explicitly.
sym = Libdl.dlsym(lib, :my_fcn) # Get a symbol for the function to call.
@ccall $sym(...) # Use the pointer `sym` instead of the library.symbol tuple.
Libdl.dlclose(lib) # Close the library explicitly.
@ccall을 입력과 함께 쓸 때 (예: @ccall "./my_lib.so".my_fcn(...)::Cvoid) 라이브러리는 암시적으로 열리고 명시적으로 닫히지 않을 수 있다는 점에 주의하세요.
가변 인자 함수 호출 (Variadic function calls)
가변 인자 C 함수를 호출하려면 인자 목록에서 세미콜론을 사용해 필수 인자와 가변 인자를 구분할 수 있어요. printf 함수를 사용한 예시가 아래에 있어요.
julia> @ccall printf("%s = %d\n"::Cstring ; "foo"::Cstring, foo::Cint)::Cint
foo = 3
8
ccall 인터페이스 (ccall interface)
@ccall에 대한 또 다른 대안 인터페이스가 있어요. 이 인터페이스는 약간 덜 편리하지만 호출 규약(calling convention)을 지정할 수 있게 해 줘요.
ccall의 인자는 다음과 같아요.
(:function, "library")쌍 (가장 흔함), OR:function이름 심볼 또는"function"이름 문자열 (현재 프로세스나 libc의 심볼용), OR 함수 포인터 (예:dlsym에서).- 함수의 반환 타입
- 함수 시그니처에 대응하는 입력 타입의 튜플. 흔한 실수 하나는 1-튜플의 인자 타입을 후행 쉼표와 함께 써야 한다는 것을 잊는 것이에요.
- 함수에 전달할 실제 인자 값들. 있으면; 각각은 별도의 매개변수.
참고:
(:function, "library")쌍과 입력 타입 목록은 문법적 튜플이어야 해요 (즉, 변수나Tuple타입의 값일 수 없어요).
rettype과 인자 타입 값은 런타임이 아니라 포함하는 메서드가 정의될 때 평가돼요.
함수 이름 vs 포인터 문법 : ccall의 첫 번째 인자의 문법은 이름으로 호출하는지 포인터로 호출하는지를 결정해요.
- 이름 기반 호출(튜플 리터럴 문법):
- 함수와 라이브러리 이름 모두 따옴표 붙은
Symbol,String, 변수 이름(GlobalRef), 또는 변수 이름으로 끝나는 점(.) 표현식일 수 있음. - 단일 이름:
(:function_name,)또는"function_name"- 기본 라이브러리 조회 사용. - 라이브러리가 있는 이름:
(:function_name, "library")- 함수와 라이브러리를 모두 지정. - 심볼, 문자열, 튜플 리터럴 상수(그 상수로 평가되는 표현식이 아니라 실제 리터럴)는 자동으로 튜플 형태로 정규화됨.
- 함수와 라이브러리 이름 모두 따옴표 붙은
- 포인터 기반 호출(비튜플 문법):
- 위에서 지정한 리터럴 튜플 표현식이 아닌 것은 무엇이든 런타임에 함수 포인터로 평가되는 표현식으로 가정됨.
- 함수 포인터 변수:
fptr(여기서 fptr은 런타임 포인터 값). - 함수 포인터 계산:
dlsym(:something)(보통 일부 캐싱 로직과 함께 매번 런타임에 계산됨).
- 라이브러리 이름 표현식:
- 변수로 주어지면 라이브러리 이름은
Symbol,String또는 다른 어떤 값으로든 해석될 수 있음. 런타임은 그 값에 대해Libdl.dlopen(name)을 지정되지 않은 횟수만큼 호출하고 결과를 캐시해요. 바인딩의 값이 변하거나 정의되지 않게 되어도, 과거나 미래 세계에 그 바인딩에 대한 값이 존재하는 한 이 결과는 무효화되지 않고 그 값이 쓰일 수 있어요. A.B().c같은 점 표현식은 마지막c까지 메서드 정의 시점에 실행돼요. 첫 번째 부분은Module로, 두 번째 부분은 따옴표 붙은 심볼로 해석되어야 해요. 그 전역의 값은ccall이 처음 실행될 때 런타임에 해석돼요.
- 변수로 주어지면 라이브러리 이름은
매크로와 함수 인터페이스 사이의 번역 표는 아래에 있어요.
| ccall (function) | @ccall (macro) |
|---|---|
ccall((:clock, "libc"), Int32, ()) |
@ccall libc.clock()::Int32 |
ccall((:clock, "libc"), Int32, ()) |
@ccall clock()::Int32 |
ccall((:clock, "libc"), Int32, ()) |
@ccall "libc".clock()::Int32 |
ccall((:gethostname, "libc"), Int32, (Ptr{UInt8}, UInt32), hn, length(hn)) |
@ccall "libc".gethostname(hn::Ptr{UInt8}, length(hn)::UInt32)::Int32 |
ccall((:clock, "libc"), Int32, ()) |
@ccall $fptr()::Int32 |
호출 규약 (Calling Convention)
ccall의 두 번째 인자(반환 타입 바로 앞)는 선택적으로 호출 규약 지정자일 수 있어요 (@ccall 매크로는 현재 호출 규약을 주는 것을 지원하지 않아요). 지정자 없이 플랫폼 기본 C 호출 규약이 사용돼요. 다른 지원되는 규약은 stdcall, cdecl, fastcall, thiscall(64비트 Windows에서는 no-op)이에요. 예를 들어 (base/libc.jl에서) 위와 같은 gethostname ccall을 Windows에 대한 올바른 시그니처로 볼 수 있어요.
hn = Vector{UInt8}(undef, 256)
err = ccall(:gethostname, stdcall, Int32, (Ptr{UInt8}, UInt32), hn, length(hn))
자세한 내용은 LLVM Language Reference를 보세요.
llvmcall이라는 추가적인 특별한 호출 규약이 하나 더 있어요. LLVM 내장 함수(intrinsics)에 대한 호출을 직접 삽입할 수 있게 해 줘요. GPGPU 같은 특이한 플랫폼을 대상으로 할 때 특히 유용할 수 있어요. 예를 들어 CUDA에서는 스레드 인덱스를 읽을 수 있어야 해요.
ccall("llvm.nvvm.read.ptx.sreg.tid.x", llvmcall, Int32, ())
다른 ccall과 마찬가지로 인자 시그니처를 정확히 맞추는 것이 필수적이에요. 또한 Core.Intrinsics가 노출하는 동등한 줄리아 함수와 달리, 내장 함수가 현재 대상에서 말이 되고 동작하는지 보장하는 호환성 레이어가 없다는 점에 유의하세요.
전역 변수 접근 (Accessing Global Variables)
네이티브 라이브러리가 내보내는 전역 변수는 cglobal 함수로 이름으로 접근할 수 있어요. cglobal의 인자는 ccall이 사용하는 것과 동일한 심볼 명세와, 변수에 저장된 값을 설명하는 타입이에요.
julia> cglobal((:errno, :libc), Int32)
Ptr{Int32} @0x00007f418d0816b8
결과는 값의 주소를 주는 포인터예요. 값은 unsafe_load와 unsafe_store!로 이 포인터를 통해 조작할 수 있어요.
참고: 이
errno심볼은 "libc"라는 이름의 라이브러리에서 찾지 못할 수 있어요. 이것은 시스템 컴파일러의 구현 세부 사항이니까요. 일반적으로 표준 라이브러리 심볼은 이름으로만 접근해 컴파일러가 올바른 것을 채우게 해야 해요. 하지만 이 예시에 보이는errno심볼은 대부분의 컴파일러에서 특별해서, 여기 보이는 값은 아마 당신이 기대하거나 원하는 것이 아닐 거예요. 멀티스레드 지원 시스템에서 C로 동등한 코드를 컴파일하면 보통 실제로 다른 함수를 호출하고(매크로 전처리기 오버로딩을 통해), 여기 출력된 기존 값과는 다른 결과를 낼 수 있어요.
포인터를 통한 데이터 접근 (Accessing Data through a Pointer)
다음 메서드들은 "안전하지 않은(unsafe)" 것으로 설명되는데, 잘못된 포인터나 타입 선언이 줄리아를 갑자기 종료시킬 수 있기 때문이에요.
Ptr{T}가 주어지면, 일반적으로 unsafe_load(ptr, [index])로 참조된 메모리에서 타입 T의 내용을 줄리아 객체로 복사할 수 있어요. index 인자는 선택적이며(기본값은 1), 줄리아 관례의 1-기반 인덱싱을 따릅니다. 이 함수는 의도적으로 getindex와 setindex!의 동작과 유사해요 (예: [] 접근 문법).
반환 값은 참조된 메모리 내용의 복사본을 담도록 초기화된 새 객체가 돼요. 참조된 메모리는 안전하게 해제되거나 놓여질 수 있어요.
T가 Any이면 메모리가 줄리아 객체에 대한 참조(jl_value_t*)를 담고 있다고 가정돼요. 결과는 이 객체에 대한 참조가 되고 객체는 복사되지 않아요. 이 경우 객체가 항상 가비지 컬렉터에 보이도록 해야 해요 (포인터는 안 되지만 새 참조는 됨). 메모리가 조기에 해제되지 않도록요. 객체가 원래 줄리아가 할당한 것이 아니면, 새 객체는 줄리아의 가비지 컬렉터로 결코 파이널라이즈되지 않는다는 점에 유의하세요. Ptr 자체가 실제로 jl_value_t*라면 unsafe_pointer_to_objref(ptr)로 줄리아 객체 참조로 다시 변환할 수 있어요. (줄리아 값 v는 pointer_from_objref(v)로 Ptr{Cvoid}로서 jl_value_t* 포인터로 변환될 수 있어요.)
역방향 연산(Ptr{T}에 데이터 쓰기)은 unsafe_store!(ptr, value, [index])로 수행할 수 있어요. 현재 이것은 원시 타입이나 다른 포인터-프리(isbits) 불변 struct 타입에 대해서만 지원돼요.
오류를 던지는 연산은 아마 현재 구현되지 않은 것이고, 해결될 수 있도록 버그로 보고되어야 해요.
관심 있는 포인터가 일반 데이터 배열(원시 타입 또는 불변 struct)이면, unsafe_wrap(Array, ptr, dims, own = false) 함수가 더 유용할 수 있어요. 마지막 매개변수는 줄리아가 밑에 있는 버퍼의 "소유권을 취하고" 반환된 Array 객체가 파이널라이즈될 때 free(ptr)를 호출해야 하면 true여야 해요. own 매개변수가 생략되거나 false이면, 호출자는 모든 접근이 완료될 때까지 버퍼가 존재하도록 해야 해요.
줄리아에서 Ptr 타입에 대한 산술(예: + 사용)은 C의 포인터 산술과 같게 동작하지 않아요. 줄리아에서 Ptr에 정수를 더하는 것은 항상 포인터를 일정한 바이트 수만큼 이동시키지, 요소만큼 이동시키지 않아요. 이렇게 해서 포인터 산술로 얻은 주소 값이 포인터의 요소 타입에 의존하지 않게 돼요.
스레드 안전성 (Thread-safety)
일부 C 라이브러리는 다른 스레드에서 콜백을 실행하는데, 줄리아는 스레드 안전하지 않으므로 추가 예방 조치가 필요해요. 특히 두 층 시스템을 설정해야 해요. C 콜백은 (줄리아의 이벤트 루프를 통해) "실제" 콜백의 실행만 예약해야 해요. 이를 위해 AsyncCondition 객체를 만들고 그것을 기다려요.
cond = Base.AsyncCondition()
wait(cond)
C에 전달하는 콜백은 cond.handle을 인자로 전달하는 :uv_async_send에 대한 ccall만 실행해야 하고, 할당이나 줄리아 런타임과의 다른 상호작용은 피해야 해요.
이벤트는 병합될 수 있다는 점에 유의하세요. uv_async_send에 대한 여러 호출이 조건에 대한 단일 깨움(wakeup) 알림으로 이어질 수 있어요.
콜백에 대해 더 알아보기 (More About Callbacks)
C 라이브러리에 콜백을 전달하는 방법에 대한 자세한 내용은 이 블로그 포스트를 보세요.
C++ (C++)
C++ 바인딩을 만드는 도구는 CxxWrap 패키지를 보세요.
- [1] C와 줄리아 모두에서 비라이브러리 함수 호출은 인라인될 수 있어 공유 라이브러리 함수 호출보다 오버헤드가 더 적을 수 있어요. 위의 요점은 실제로 외부 함수 호출을 하는 비용이 어느 네이티브 언어에서 호출하는 것과 대략 같다는 것이에요.
- [2] Clang 패키지를 사용하면 C 헤더 파일에서 줄리아 코드를 자동 생성할 수 있어요.
더 알아보기 (Learn more)
- julia 공식 메뉴얼 — Calling C and Fortran Code (원문)
- ccall / @ccall 매크로 참조
- Libdl standard library
- CxxWrap.jl 패키지
C와 Fortran 호출을 이해하는 핵심은 @ccall 문법(library.func(arg::Type)::Ret), C 타입과 줄리아 타입의 정확한 대응(int≠Int, Cvoid vs Union{}), 그리고 T/Ref{T}/Ptr{T}를 언제 쓰는지 구분하는 거예요. 함수 포인터는 @cfunction으로 만들고, Fortran은 포인터 기반 전달이 필요하다는 점까지 실제 예시를 직접 따라 하면 자연스럽게 정리돼요.