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 라이브러리 함수, 줄리아 런타임의 함수, 또는 줄리아에 연결된 애플리케이션의 함수를 호출하는 데 쓰일 수 있어요. 라이브러리 생략으로 임의의 라이브러리의 함수를 호출하는 데(예: dlsymRTLD_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 라이브러리 함수 getenvenv.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.

출처: julia 공식 메뉴얼 — Calling C and Fortran Code

본문

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는 두 요소 ab에 대한 포인터를 받고, ab 앞에 나타나야 하면 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}로 변환하고, 요소 타입의 바이트 크기를 계산하는 등의 일을 처리해 주는 것에 주목하세요.

재미로, mycompareprintln("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_convertPtr 타입으로의 변환을 처리해요. 객체를 네이티브 포인터로 변환하면 가비지 컬렉터로부터 객체를 숨겨 조기에 해제될 수 있기 때문에 안전하지 않은 것으로 간주돼요.

타입 대응 (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}을 쓸 수 있어요. Cstringccall의 반환 타입으로도 쓸 수 있는데, 그 경우 당연히 추가 검사를 도입하지 않고 호출의 가독성만 높이기 위한 것이에요.

시스템 의존 타입 (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)));
}

다음 줄리아 코드는 ccalldist를 호출해요.

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에서는 지원되지 않음
  • va_arg
    • ccall이나 @cfunction에서 지원되지 않음

@ccall / @cfunction 반환 타입 번역 가이드

C 반환 타입을 줄리아로 번역하려면:

  • void
    • Cvoid (이것은 싱글턴 인스턴스 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에 대응하는 줄리아 타입)
  • 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

반환 시 widthrange의 내용은(만약 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은 값으로 전달되므로 함수의 입력 시그니처는 RefPtr 없이 그냥 ::Csize_t로 선언돼요. (래퍼가 대신 Fortran 함수를 호출했다면, Fortran 변수는 포인터로 전달되므로 대응하는 함수 입력 시그니처는 ::Ref{Csize_t}가 되었을 거예요.) 게다가 nCsize_t 정수로 변환 가능한 어떤 타입이든 될 수 있어요. ccallBase.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_loadString 같은 줄리아의 많은 메서드는 버퍼의 소유권을 가지는 대신 데이터의 복사본을 만들어, 줄리아에 영향을 주지 않고 원본 데이터를 해제(또는 변경)하는 것이 안전해요. 주목할 예외는 unsafe_wrap인데, 성능상의 이유로 밑에 있는 버퍼를 공유(또는 소유권을 가지라고 지시될 수 있음)해요.

가비지 컬렉터는 파이널라이제이션(finalization)의 어떤 순서도 보장하지 않아요. 즉 ab에 대한 참조를 담고 있고 ab 모두 가비지 컬렉션 대상이면, ba 이후에 파이널라이즈된다는 보장이 없어요. a의 적절한 파이널라이제이션이 b의 유효성에 달려 있다면, 다른 방식으로 처리해야 해요.

비상수 함수 명세 (Non-constant Function Specifications)

어떤 경우에는 필요한 라이브러리의 정확한 이름이나 경로를 미리 알 수 없어 런타임에 계산해야 해요. 이런 경우를 처리하려면 라이브러리 구성 요소 명세가 Libdl.LazyLibrary 같은 값일 수 있어요. 런타임이 ccall로 처음 사용될 때 그 객체에 대해 Libdl.dlopen을 호출해요.

LazyLoading을 위한 LazyLibrary 사용 (Using LazyLibrary for Lazy Loading)

Libdl.LazyLibrary는 첫 사용까지 라이브러리 로딩을 연기하는 스레드 안전 메커니즘을 제공해요. 현대 줄리아 코드에서 라이브러리 초기화의 권장 접근 방식이에요.

LazyLibraryccall(), @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_loadunsafe_store!로 이 포인터를 통해 조작할 수 있어요.

참고:errno 심볼은 "libc"라는 이름의 라이브러리에서 찾지 못할 수 있어요. 이것은 시스템 컴파일러의 구현 세부 사항이니까요. 일반적으로 표준 라이브러리 심볼은 이름으로만 접근해 컴파일러가 올바른 것을 채우게 해야 해요. 하지만 이 예시에 보이는 errno 심볼은 대부분의 컴파일러에서 특별해서, 여기 보이는 값은 아마 당신이 기대하거나 원하는 것이 아닐 거예요. 멀티스레드 지원 시스템에서 C로 동등한 코드를 컴파일하면 보통 실제로 다른 함수를 호출하고(매크로 전처리기 오버로딩을 통해), 여기 출력된 기존 값과는 다른 결과를 낼 수 있어요.

포인터를 통한 데이터 접근 (Accessing Data through a Pointer)

다음 메서드들은 "안전하지 않은(unsafe)" 것으로 설명되는데, 잘못된 포인터나 타입 선언이 줄리아를 갑자기 종료시킬 수 있기 때문이에요.

Ptr{T}가 주어지면, 일반적으로 unsafe_load(ptr, [index])로 참조된 메모리에서 타입 T의 내용을 줄리아 객체로 복사할 수 있어요. index 인자는 선택적이며(기본값은 1), 줄리아 관례의 1-기반 인덱싱을 따릅니다. 이 함수는 의도적으로 getindexsetindex!의 동작과 유사해요 (예: [] 접근 문법).

반환 값은 참조된 메모리 내용의 복사본을 담도록 초기화된 새 객체가 돼요. 참조된 메모리는 안전하게 해제되거나 놓여질 수 있어요.

TAny이면 메모리가 줄리아 객체에 대한 참조(jl_value_t*)를 담고 있다고 가정돼요. 결과는 이 객체에 대한 참조가 되고 객체는 복사되지 않아요. 이 경우 객체가 항상 가비지 컬렉터에 보이도록 해야 해요 (포인터는 안 되지만 새 참조는 됨). 메모리가 조기에 해제되지 않도록요. 객체가 원래 줄리아가 할당한 것이 아니면, 새 객체는 줄리아의 가비지 컬렉터로 결코 파이널라이즈되지 않는다는 점에 유의하세요. Ptr 자체가 실제로 jl_value_t*라면 unsafe_pointer_to_objref(ptr)로 줄리아 객체 참조로 다시 변환할 수 있어요. (줄리아 값 vpointer_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)

C와 Fortran 호출을 이해하는 핵심은 @ccall 문법(library.func(arg::Type)::Ret), C 타입과 줄리아 타입의 정확한 대응(intInt, Cvoid vs Union{}), 그리고 T/Ref{T}/Ptr{T}를 언제 쓰는지 구분하는 거예요. 함수 포인터는 @cfunction으로 만들고, Fortran은 포인터 기반 전달이 필요하다는 점까지 실제 예시를 직접 따라 하면 자연스럽게 정리돼요.