呼叫 C 和 Fortran 程式碼

儘管大多數程式碼可以用 Julia 編寫,但已經有許多高品質、成熟的函式庫用 C 和 Fortran 編寫,可用於數值運算。為了讓這些現有程式碼易於使用,Julia 讓呼叫 C 和 Fortran 函式變得簡單且有效率。Julia 有一個「無樣板程式碼」哲學:函式可以直接從 Julia 呼叫,而不需要任何「黏合」程式碼、程式碼產生或編譯,甚至可以直接從互動式提示字元呼叫。這只要透過使用 @ccall 巨集(或較不方便的 ccall 語法,請參閱 ccall 語法區段)進行適當的呼叫即可達成。

要呼叫的程式碼必須以共用函式庫的形式提供。大多數 C 和 Fortran 函式庫都已經編譯為共用函式庫,但如果你使用 GCC(或 Clang)自行編譯程式碼,則需要使用 -shared-fPIC 選項。Julia 的 JIT 產生的機器指令與原生 C 呼叫相同,因此產生的開銷與從 C 程式碼呼叫函式庫函式相同。[1]

預設情況下,Fortran 編譯器會 產生混淆名稱(例如,將函式名稱轉換為小寫或大寫,通常會附加底線),因此要呼叫 Fortran 函式,你必須傳遞與 Fortran 編譯器遵循的規則對應的混淆識別碼。此外,在呼叫 Fortran 函式時,所有輸入都必須傳遞為堆疊或堆積體上已配置值的指標。這不僅適用於通常會配置在堆積體上的陣列和其他可變物件,也適用於通常會配置在堆疊上並在使用 C 或 Julia 呼叫慣例時通常會傳遞至暫存器的純量值,例如整數和浮點數。

用於產生呼叫函式庫函式的 @ccall 語法為

  @ccall library.function_name(argvalue1::argtype1, ...)::returntype
  @ccall function_name(argvalue1::argtype1, ...)::returntype
  @ccall $function_pointer(argvalue1::argtype1, ...)::returntype

其中 library 是字串常數或字面值(但請參閱下方的 非常數函式規格)。可以省略函式庫,這樣函式名稱就會在目前的處理程序中解析。此表單可用於呼叫 C 函式庫函式、Julia 執行時間中的函式或連結至 Julia 的應用程式中的函式。也可以指定函式庫的完整路徑。或者,@ccall 也可用於呼叫函式指標 $function_pointer,例如由 Libdl.dlsym 傳回的指標。argtypes 對應到 C 函式簽章,而 argvalues 是要傳遞給函式的實際引數值。

注意

請參閱下方以了解如何 將 C 型別對應至 Julia 型別

以下是一個完整但簡單的範例,它會在大部分衍生自 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 用法包覆在 Julia 函式中,這些函式會設定引數,然後以 C 或 Fortran 函式指定的方式檢查錯誤。如果發生錯誤,則會將其擲回為正常的 Julia 例外。這特別重要,因為 C 和 Fortran API 在如何指出錯誤條件方面出了名的不一致。例如,getenv C 函式庫函式會包覆在下列 Julia 函式中,這是 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 來使用主機名稱填入陣列。最後,它會取得一個指向主機名稱緩衝區的指標,並將指標轉換成 Julia 字串,假設它是一個以 Null 結束的 C 字串。

C 函式庫通常會使用這種模式,要求呼叫者配置要傳遞給被呼叫者並填入的記憶體。從 Julia 配置記憶體通常是透過建立一個未初始化的陣列,並將一個指向其資料的指標傳遞給 C 函數來完成。這就是我們在此不使用 Cstring 類型的緣故:由於陣列未初始化,它可能會包含 Null 位元組。將其轉換成 Cstring 作為 @ccall 的一部分會檢查是否包含 Null 位元組,因此可能會擲回一個轉換錯誤。

使用 unsafe_string 取消參考 pointer(hostname) 是個不安全的作業,因為它需要存取已配置給 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 的 Julia 函數指標

可以將 Julia 函數傳遞給接受函數指標引數的原生 C 函數。例如,若要符合以下形式的 C 原型

typedef returntype (*functiontype)(argumenttype, ...)

巨集 @cfunction 會為呼叫 Julia 函數產生相容於 C 的函數指標。 @cfunction 的引數為

  1. Julia 函數
  2. 函數的傳回類型
  3. 輸入類型的組,對應於函數簽章
注意

@ccall 一樣,傳回類型和輸入類型必須是文字常數。

注意

目前僅支援平台預設的 C 呼叫慣例。這表示 @cfunction 產生的指標無法在 32 位元 Windows 上的 WINAPI 預期 stdcall 函數的呼叫中使用,但可以在 WIN64(其中 stdcall 與 C 呼叫慣例統一)上使用。

注意

透過 @cfunction 公開的回呼函數不應擲回錯誤,因為這會意外地將控制權傳回 Julia 執行時期,並可能使程式處於未定義的狀態。

一個經典的範例是標準 C 函式庫 qsort 函數,宣告如下

void qsort(void *base, size_t nitems, size_t size,
           int (*compare)(const void*, const void*));

base 引數是指向長度為 nitems 的陣列的指標,其中每個元素有 size 位元組。compare 是回呼函數,它會取得指標指向兩個元素 ab,並傳回一個小於/大於零的整數,表示 a 應出現在 b 之前/之後(或為零,表示允許任何順序)。

現在,假設我們有一個 1 維陣列 A,其中包含我們想要使用 qsort 函數(而不是 Julia 內建的 sort 函數)進行排序的 Julia 值。在我們考慮呼叫 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 需要三個參數:Julia 函數 (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

如範例所示,原始 Julia 陣列 A 現在已排序:[-2.7, 1.3, 3.1, 4.4]。請注意,Julia 會負責將陣列轉換為 Ptr{Cdouble}),計算元素類型的位元組大小,等等。

為了好玩,請嘗試在 mycompare 中插入一行 println("mycompare($a, $b)"),這將讓您看到 qsort 執行的比較(並驗證它是否真的呼叫您傳遞給它的 Julia 函數)。

將 C 類型對應至 Julia

必須將宣告的 C 類型與其在 Julia 中的宣告完全相符。不一致可能會導致在一個系統上正確運作的程式碼在另一個系統上失敗或產生不確定的結果。

請注意,在呼叫 C 函數的過程中,不會在任何地方使用 C 標頭檔:您有責任確保您的 Julia 類型和呼叫簽章準確反映 C 標頭檔中的類型和呼叫簽章。[2]

自動類型轉換

Julia 會自動插入對 Base.cconvert 函數的呼叫,以將每個引數轉換為指定的類型。例如,下列呼叫

@ccall "libfoo".foo(x::Int32, y::Float64)::Cvoid

將會表現得好像是以這種方式撰寫的

@ccall "libfoo".foo(
    Base.unsafe_convert(Int32, Base.cconvert(Int32, x))::Int32,
    Base.unsafe_convert(Float64, Base.cconvert(Float64, y))::Float64
    )::Cvoid

Base.cconvert 通常只會呼叫 convert,但可以定義為傳回一個任意的新物件,更適合傳遞給 C。這應該用於執行所有將由 C 程式碼存取的記憶體配置。例如,這用於將物件(例如字串)的 Array 轉換為指標陣列。

Base.unsafe_convert 處理轉換為 Ptr 類型。它被視為不安全的,因為將物件轉換為原生指標可能會將物件隱藏在垃圾回收器中,導致它過早被釋放。

類型對應

首先,讓我們回顧一些相關的 Julia 類型術語

語法 / 關鍵字範例說明
可變結構BitSet「葉類型」:: 一組相關資料,其中包含類型標記,由 Julia GC 管理,並由物件識別定義。葉類型的類型參數必須完全定義(不允許 TypeVars),才能建構實例。
抽象類型AnyAbstractArray{T, N}Complex{T}「超級類型」:: 無法實例化,但可用於描述一組類型的超級類型(非葉類型)。
T{A}Vector{Int}「類型參數」:: 類型的專業化(通常用於調度或儲存最佳化)。
「類型變數」:: 類型參數宣告中的 T 稱為類型變數(類型變數的簡稱)。
基本類型IntFloat64「基本類型」:: 沒有欄位但有大小的類型。它以按值儲存和定義。
結構Pair{Int, Int}「結構」:: 所有欄位定義為常數的類型。它以按值定義,並可能與類型標記一起儲存。
ComplexF64 (isbits)「is-bits」:: 基本類型或所有欄位都是其他 isbits 類型的 結構 類型。它以按值定義,並在沒有類型標記的情況下儲存。
struct ...; endnothing「單例」:: 沒有欄位的葉類型或結構。
(...)tuple(...)(1, 2, 3)「元組」:: 類似於匿名結構類型或常數陣列的不可變資料結構。表示為陣列或結構。

位元類型

有幾個特殊類型需要注意,因為無法定義其他類型具有相同的行為

  • 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 子類型的 Julia 類型都假設為無符號。

  • Ref{T}

    表現得像一個 Ptr{T},它可以使用 Julia GC 管理其記憶體。

  • Array{T,N}

    當一個陣列作為 Ptr{T} 參數傳遞給 C 時,它不會被重新詮釋:Julia 要求陣列的元素類型與 T 匹配,並且傳遞第一個元素的地址。

    因此,如果一個 Array 包含格式錯誤的資料,則必須使用類似於 trunc.(Int32, A) 的呼叫來明確轉換它。

    要將陣列 A 作為不同類型的指標傳遞(事先不轉換資料,例如將 Float64 陣列傳遞給對未詮釋位元組進行操作的函式),可以將參數宣告為 Ptr{Cvoid}

    如果將 eltype 為 Ptr{T} 的陣列作為 Ptr{Ptr{T}} 參數傳遞,Base.cconvert 將嘗試首先製作陣列的空終止副本,每個元素都替換為其 Base.cconvert 版本。例如,這允許將類型為 Vector{String}argv 指標陣列傳遞給類型為 Ptr{Ptr{Cchar}} 的參數。

在我們目前支援的所有系統上,基本的 C/C++ 值類型可以轉換為 Julia 類型,如下所示。每個 C 類型還有一個對應的 Julia 類型,其名稱相同,並加上 C 前綴。這有助於撰寫可攜式程式碼(並記住 C 中的 int 與 Julia 中的 Int 不同)。

系統獨立類型

C 名稱Fortran 名稱標準 Julia 別名Julia 基礎類型
unsigned charCHARACTERCucharUInt8
bool (_Bool in C99+)CucharUInt8
shortINTEGER*2, LOGICAL*2CshortInt16
unsigned shortCushortUInt16
int, BOOL (C, 典型)INTEGER*4, LOGICAL*4CintInt32
unsigned intCuintUInt32
long longINTEGER*8, LOGICAL*8ClonglongInt64
unsigned long longCulonglongUInt64
intmax_tCintmax_tInt64
uintmax_tCuintmax_tUInt64
floatREAL*4iCfloatFloat32
doubleREAL*8CdoubleFloat64
complex floatCOMPLEX*8ComplexF32Complex{Float32}
complex doubleCOMPLEX*16ComplexF64Complex{Float64}
ptrdiff_tCptrdiff_tInt
ssize_tCssize_tInt
size_tCsize_tUInt
voidCvoid
void[[noreturn]]_NoreturnUnion{}
void*Ptr{Cvoid}(或類似地 Ref{Cvoid}
T*(其中 T 表示適當定義的類型)Ref{T}(僅當 T 是 isbits 類型時,才能安全地變異 T)
char*(或 char[],例如字串)CHARACTER*N如果以 null 結束,則為 Cstring,否則為 Ptr{UInt8}
char**(或 *char[]Ptr{Ptr{UInt8}}
jl_value_t*(任何 Julia 類型)任何
jl_value_t* const*(對 Julia 值的參照)Ref{Any}(常數,因為變異需要寫入屏障,而無法正確插入)
va_arg不支援
...(可變函數規格)T...(其中 T 是上述類型之一,使用 ccall 函數時)
...(可變函數規格); va_arg1::T, va_arg2::S, 等。(僅支援 @ccall 巨集)

Cstring 類型基本上是 Ptr{UInt8} 的同義詞,但如果 Julia 字串包含任何嵌入的 null 字元(如果 C 常式將 null 視為終止符,則會導致字串被靜默截斷),則轉換為 Cstring 會擲回錯誤。如果您要將 char* 傳遞給不假設 null 結束的 C 常式(例如,因為您傳遞明確的字串長度),或者如果您確定 Julia 字串不包含 null 並且想要略過檢查,則可以使用 Ptr{UInt8} 作為引數類型。Cstring 也可以用作 ccall 回傳類型,但在這種情況下,它顯然不會引入任何額外的檢查,並且僅用於提高呼叫的可讀性。

系統依賴類型

C 名稱標準 Julia 別名Julia 基礎類型
charCcharInt8(x86、x86_64),UInt8(powerpc、arm)
longClongInt(UNIX),Int32(Windows)
unsigned longCulongUInt(UNIX),UInt32(Windows)
wchar_tCwchar_tInt32 (UNIX)、UInt16 (Windows)
注意

呼叫 Fortran 時,所有輸入都必須透過指標傳遞至堆疊或堆積配置的值,因此上述所有類型對應都應包含額外的 Ptr{..}Ref{..} 包裝器,包覆其類型規格。

警告

對於字串引數 (char*),Julia 類型應為 Cstring (如果預期為以 null 結束的資料),或 Ptr{Cchar}Ptr{UInt8} (這兩種指標類型具有相同效果),如上所述,而非 String。類似地,對於陣列引數 (T[]T*),Julia 類型也應為 Ptr{T},而非 Vector{T}

警告

Julia 的 Char 類型為 32 位元,這與所有平台上的寬字元類型 (wchar_twint_t) 不同。

警告

回傳類型為 Union{} 表示函式不會回傳,即 C++11 [[noreturn]] 或 C11 _Noreturn (例如 jl_throwlongjmp)。不要將此用於不回傳值 (void) 但會回傳的函式,請改用 Cvoid

注意

對於 wchar_t* 引數,Julia 類型應為 Cwstring (如果 C 常式預期為以 null 結束的字串),或 Ptr{Cwchar_t}。另請注意,Julia 中的 UTF-8 字串資料在內部以 null 結束,因此可以傳遞給預期為以 null 結束的資料的 C 函式,而無需複製 (但使用 Cwstring 類型會導致字串本身包含 null 字元時發生錯誤)。

注意

可以透過在 Julia 中使用 Ptr{Ptr{UInt8}} 類型來呼叫採用 char** 類型引數的 C 函式。例如,表單的 C 函式

int main(int argc, char **argv);

可透過下列 Julia 程式碼呼叫

argv = [ "a.out", "arg1", "arg2" ]
@ccall main(length(argv)::Int32, argv::Ptr{Ptr{UInt8}})::Int32
注意

對於採用變長字串類型 character(len=*) 的 Fortran 函數,字串長度會提供為隱藏參數。這些參數在清單中的類型和位置會因編譯器而異,其中編譯器供應商通常預設使用 Csize_t 作為類型,並將隱藏參數附加在參數清單的結尾。雖然這種行為已針對某些編譯器(GNU)修正,但其他編譯器選擇性地允許將隱藏參數置於字元參數之後(Intel、PGI)。例如,下列形式的 Fortran 子常式

subroutine test(str1, str2)
character(len=*) :: str1,str2

可透過下列 Julia 程式碼呼叫,其中長度會附加

str1 = "foo"
str2 = "bar"
ccall(:test, Cvoid, (Ptr{UInt8}, Ptr{UInt8}, Csize_t, Csize_t),
                    str1, str2, sizeof(str1), sizeof(str2))
警告

Fortran 編譯器可能會為指標、假設形狀(:)和假設大小(*)陣列加入其他隱藏參數。透過使用 ISO_C_BINDING 並在子常式的定義中包含 bind(c),可以避免這種行為,強烈建議將其用於可互操作的程式碼。在此情況下,不會有任何隱藏參數,但會犧牲一些語言功能(例如,只有 character(len=1) 會被允許傳遞字串)。

注意

宣告為傳回 Cvoid 的 C 函數會在 Julia 中傳回 nothing 值。

結構類型對應

複合類型,例如 C 中的 struct 或 Fortran90 中的 TYPE(或 F77 中的一些變體中的 STRUCTURE / RECORD),可以在 Julia 中透過建立具有相同欄位配置的 struct 定義來反映。

當遞迴使用時,isbits 類型會儲存在內聯中。所有其他類型都會儲存為指向資料的指標。在 C 中,當鏡像另一個結構中按值使用的結構時,絕對不要嘗試手動複製欄位,因為這不會保留正確的欄位對齊。相反地,宣告一個 isbits 結構類型,並改用它。在轉換為 Julia 時,無法使用未命名結構。

Julia 不支援封包結構和聯合宣告。

如果你事先知道具有最大大小的欄位(可能包括填補),則可以取得 union 的近似值。在將欄位轉換為 Julia 時,宣告 Julia 欄位僅為該類型。

可以使用 NTuple 表示參數陣列。例如,C 表示法中的結構寫成

struct B {
    int A[3];
};

b_a_2 = B.A[2];

可以在 Julia 中寫成

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 的變長結構)。處理這些的最佳方式通常是直接處理位元組偏移量。例如,如果 C 函式庫宣告一個適當的字串類型並傳回指向它的指標

struct String {
    int strlen;
    char data[];
};

在 Julia 中,我們可以獨立存取各個部分來複製該字串

str = from_c::Ptr{Cvoid}
len = unsafe_load(Ptr{Cint}(str))
unsafe_string(str + Core.sizeof(Cint), len)

類型參數

當定義包含使用情況的方法時,@ccall@cfunction 的類型引數會在靜態時評估。因此,它們必須採用文字元組的形式,而不是變數,且不能參考局部變數。

這聽起來可能像一個奇怪的限制,但請記住,由於 C 不是像 Julia 這樣的動態語言,因此它的函式只能接受具有靜態已知固定簽章的引數類型。

然而,雖然類型配置必須在靜態下已知才能計算預期的 C ABI,但函數的靜態參數被視為此靜態環境的一部分。函數的靜態參數可以用作呼叫簽章中的類型參數,只要它們不影響類型的配置即可。例如,f(x::T) where {T} = @ccall valid(x::Ptr{T})::Ptr{T} 是有效的,因為 Ptr 永遠是字元大小的原始類型。但是,g(x::T) where {T} = @ccall notvalid(x::T)::T 無效,因為 T 的類型配置在靜態下並未已知。

SIMD 值

注意:此功能目前僅在 64 位元 x86 和 AArch64 平台上實作。

如果 C/C++ 常式有引數或回傳值是原生 SIMD 類型,對應的 Julia 類型是 VecElement 的同質元組,自然會對應到 SIMD 類型。特別是

  • 元組必須與 SIMD 類型大小相同。例如,表示 x86 上的 __m128 的元組大小必須為 16 位元組。
  • 元組的元素類型必須是 VecElement{T} 的實例,其中 T 是 1、2、4 或 8 位元組的原始類型。

例如,考慮這個使用 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)));
}

以下 Julia 程式碼使用 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 的主機上無法執行。

記憶體擁有權

malloc/free

此類物件的記憶體配置與解除配置必須透過呼叫所使用函式庫中適當的清除常式來處理,就像在任何 C 程式中一樣。不要嘗試使用 Julia 中的 Libc.free 來釋放從 C 函式庫接收的物件,因為這可能會導致透過錯誤的函式庫呼叫 free 函式,並導致程序中止。反之(傳遞在 Julia 中配置的物件,由外部函式庫釋放)也同樣無效。

何時使用 TPtr{T}Ref{T}

在包裝對外部 C 常式的呼叫的 Julia 程式碼中,一般(非指標)資料應宣告為 @ccall 內部的 T 型別,因為它們是按值傳遞。對於接受指標的 C 程式碼,通常應對輸入參數的型別使用 Ref{T},允許使用指標指向由 Julia 或 C 管理的記憶體,透過對 Base.cconvert 的隱式呼叫。相反地,由所呼叫的 C 函式傳回的指標應宣告為輸出型別 Ptr{T},反映由 C 管理所指向的記憶體。包含在 C 結構中的指標應表示為 Ptr{T} 型別的欄位,在對應的 Julia 結構型別中,用來模仿對應 C 結構的內部結構。

在 Julia 程式碼中封裝對外部 Fortran 常式的呼叫時,所有輸入參數都應該宣告為 Ref{T} 類型,因為 Fortran 會將所有變數傳遞為指向記憶體位置的指標。回傳類型應該為 Fortran 子常式的 Cvoid,或回傳類型為 T 的 Fortran 函式的 T

將 C 函式對應至 Julia

@ccall / @cfunction 參數轉換指南

將 C 參數清單轉換為 Julia

  • T,其中 T 是其中一種基本類型:charintlongshortfloatdoublecomplexenum 或其任何 typedef 等效項

    • T,其中 T 是等效的 Julia 位元類型(根據上表)
    • 如果 Tenum,參數類型應該等效於 CintCuint
    • 參數值將會複製(傳遞值)
  • struct T(包含 typedef 到結構)

    • T,其中 T 是 Julia 葉類型
    • 參數值將會複製(傳遞值)
  • void*

    • 取決於此參數如何使用,首先將其轉換為預期的指標類型,然後使用此清單中的其餘規則來確定 Julia 等效項
    • 如果此參數真的是未知指標,則可以將其宣告為 Ptr{Cvoid}
  • jl_value_t*

    • 任何
    • 參數值必須是有效的 Julia 物件
  • jl_value_t* const*

    • Ref{Any}
    • 參數清單必須是有效的 Julia 物件(或 C_NULL
    • 不能用於輸出參數,除非使用者能夠另外安排讓物件保持 GC
  • T*

    • Ref{T},其中 T 是對應於 T 的 Julia 類型
    • 如果參數值是 inlinealloc 類型(包括 isbits),則會複製該值;否則,該值必須是有效的 Julia 物件
  • T (*)(...)(例如函式的指標)

    • Ptr{Cvoid}(您可能需要明確使用 @cfunction 來建立此指標)
  • ...(例如變數參數)

    • [對於 ccall]:T...,其中 T 是所有剩餘參數的單一 Julia 類型
    • [對於 @ccall]:; va_arg1::T, va_arg2::S, etc,其中 TS 是 Julia 類型(即使用 ; 將常規參數與變數參數分開)
    • 目前不受 @cfunction 支援
  • va_arg

    • 不受 ccall@cfunction 支援

@ccall / @cfunction 回傳類型轉換指南

用於將 C 回傳類型轉換為 Julia

  • void

    • Cvoid(這將回傳單例實體 nothing::Cvoid
  • T,其中 T 是其中一種基本類型:charintlongshortfloatdoublecomplexenum 或其任何 typedef 等效項

    • T,其中 T 是等效的 Julia 位元類型(根據上表)
    • 如果 Tenum,參數類型應該等效於 CintCuint
    • 參數值將被複製(以值傳回)
  • struct T(包含 typedef 到結構)

    • T,其中 T 是 Julia 葉類型
    • 參數值將被複製(以值傳回)
  • void*

    • 取決於此參數如何使用,首先將其轉換為預期的指標類型,然後使用此清單中的其餘規則來確定 Julia 等效項
    • 如果此參數真的是未知指標,則可以將其宣告為 Ptr{Cvoid}
  • jl_value_t*

    • 任何
    • 參數值必須是有效的 Julia 物件
  • jl_value_t**

    • Ptr{Any}Ref{Any} 作為回傳類型無效)
  • T*

    • 如果記憶體已經由 Julia 擁有,或者是一個 isbits 類型,並且已知是非空值

      • Ref{T},其中 T 是對應於 T 的 Julia 類型
      • Ref{Any} 的傳回類型無效,它應該是 Any(對應到 jl_value_t*)或 Ptr{Any}(對應到 jl_value_t**
      • 如果 Tisbits 類型,C 不得 修改透過 Ref{T} 傳回的記憶體
    • 如果記憶體是由 C 所擁有

      • Ptr{T},其中 T 是對應到 T 的 Julia 類型
  • T (*)(...)(例如函式的指標)

    • Ptr{Cvoid} 若要從 Julia 直接呼叫這個,您需要將這個傳遞為 @ccall 的第一個引數。請參閱 間接呼叫

傳遞指標以修改輸入

因為 C 不支援多個傳回值,所以 C 函式通常會採用指向函式將修改的資料的指標。若要在 @ccall 內完成這個,您需要先將值封裝在適當類型的 Ref{T} 內。當您將這個 Ref 物件傳遞為引數時,Julia 會自動傳遞一個 C 指標到封裝的資料

width = Ref{Cint}(0)
range = Ref{Cfloat}(0)
@ccall foo(width::Ref{Cint}, range::Ref{Cfloat})::Cvoid

傳回時,widthrange 的內容(如果它們被 foo 所變更)可以透過 width[]range[] 擷取;也就是說,它們的作用就像零維陣列。

C 封裝範例

讓我們從一個傳回 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 結構的內部,對應的 Julia 封裝只需要一個新的類型宣告 gsl_permutation,它沒有內部欄位,其唯一目的就是放置在 Ptr 類型的類型參數中。ccall 的傳回類型宣告為 Ptr{gsl_permutation},因為由 output_ptr 分配和指向的記憶體是由 C 所控制。

輸入 n 是按值傳遞的,因此函數輸入簽章僅宣告為 ::Csize_t,無需任何 RefPtr。(如果包裝器呼叫 Fortran 函數,則對應的函數輸入簽章將為 ::Ref{Csize_t},因為 Fortran 變數是按指標傳遞的。)此外,n 可以是任何可轉換為 Csize_t 整數的類型;ccall 隱含地呼叫 Base.cconvert(Csize_t, n)

以下是包裝對應解構函數的第二個範例

# 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

以下是傳遞 Julia 陣列的第三個範例

# 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 函數實際評估的結果填入 Julia 陣列 result_array。此變數宣告為 Ref{Cdouble},因為其記憶體是由 Julia 分配和管理的。對 Base.cconvert(Ref{Cdouble}, result_array) 的隱含呼叫會將 Julia 指標解壓縮到 Julia 陣列資料結構中,轉換為 C 可以理解的形式。

Fortran Wrapper 範例

以下範例使用 ccall 呼叫 common Fortran 函式庫 (libBLAS) 中的函式來計算點積。請注意,此處的引數對應與上述範例略有不同,因為我們需要從 Julia 對應到 Fortran。在每個引數類型上,我們指定 RefPtr。此混淆慣例可能特定於您的 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

垃圾回收安全性

傳遞資料給 @ccall 時,最好避免使用 pointer 函式。請改為定義 Base.cconvert 方法,並將變數直接傳遞給 @ccall@ccall 會自動安排,讓呼叫傳回之前,其所有引數都會保留在垃圾回收中。如果 C API 會儲存 Julia 分配的記憶體參考,在 @ccall 傳回後,您必須確保物件對垃圾回收程式保持可見。建議的做法是建立一個型別為 Array{Ref,1} 的全域變數,用於儲存這些值,直到 C 函式庫通知您已完成使用這些值。

每當您建立指向 Julia 資料的指標時,您必須確保原始資料存在,直到您完成使用指標為止。Julia 中的許多方法,例如 unsafe_loadString 會複製資料,而不是取得緩衝區的所有權,因此可以安全地釋放(或變更)原始資料,而不會影響 Julia。一個值得注意的例外是 unsafe_wrap,它出於效能考量,會共用(或可告知取得)基礎緩衝區的所有權。

垃圾收集器不保證任何完成順序。亦即,如果 a 包含對 b 的參照,且 ab 都應進行垃圾收集,則無法保證 b 會在 a 之後完成。如果 a 的適當完成取決於 b 有效,則必須以其他方式處理。

非常數函式規格

在某些情況下,所需函式庫的確切名稱或路徑無法預先得知,且必須在執行階段計算。為處理此類情況,函式庫元件規格可以是函式呼叫,例如 find_blas().dgemm。當 ccall 本身執行時,將執行呼叫表達式。但是,假設函式庫位置在確定後不會變更,因此呼叫結果可以快取並重複使用。因此,表達式執行的次數未指定,且針對多個呼叫傳回不同值會導致未指定行為。

如果需要更多彈性,則可以透過分段處理 eval,將計算值用作函式名稱,如下所示

@eval @ccall "lib".$(string("a", "b"))()::Cint

此表達式使用 string 建構名稱,然後將此名稱替換為新的 @ccall 表達式,然後評估。請記住,eval 僅在頂層運作,因此在此表達式中,無法使用局部變數(除非其值已用 $ 替換)。因此,eval 通常僅用於形成頂層定義,例如在包裝包含許多類似函式的函式庫時。可以為 @cfunction 建構類似的範例。

但是,執行此操作也會非常緩慢且會洩漏記憶體,因此您通常應該避免執行此操作,而應繼續閱讀。下一節將討論如何使用間接呼叫有效達成類似的效果。

間接呼叫

@ccall 的第一個引數也可以是在執行階段評估的表達式。在這種情況下,該表達式必須評估為 Ptr,它將用作要呼叫的原生函式的位址。當第一個 @ccall 引數包含對非常數的參照(例如局部變數、函式引數或非常數全域變數)時,就會發生這種行為。

例如,您可以透過 dlsym 查詢函式,然後將其快取在該階段的共用參照中。例如

macro dlsym(lib, func)
    z = Ref{Ptr{Cvoid}}(C_NULL)
    quote
        let zlocal = $z[]
            if zlocal == C_NULL
                zlocal = dlsym($(esc(lib))::Ptr{Cvoid}, $(esc(func)))::Ptr{Cvoid}
                $z[] = zlocal
            end
            zlocal
        end
    end
end

mylibvar = Libdl.dlopen("mylib")
@ccall $(@dlsym(mylibvar, "myfunc"))()::Cvoid

封閉 cfunctions

傳遞給 @cfunction 的第一個引數可以用 $ 標記,這樣回傳值就會變成一個 struct CFunction,這個結構會封閉在引數中。你必須確保這個回傳物件在所有使用都結束後仍然存在。當這個參照被捨棄且 atexit 時,cfunction 指標中的內容和程式碼會透過 finalizer 被清除。通常不需要這樣做,因為 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)。

關閉函式庫

有時候關閉(卸載)函式庫以便重新載入會很有用。例如,在開發 C 程式碼以供 Julia 使用時,可能需要編譯、從 Julia 呼叫 C 程式碼,然後關閉函式庫、進行編輯、重新編譯,並載入新的變更。你可以重新啟動 Julia 或使用 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)時,函式庫會隱式開啟,且可能不會明確關閉。

可變參數函式呼叫

若要呼叫可變參數 C 函數,可以使用 分號 在參數清單中將必要參數與可變參數分開。以下提供使用 printf 函數的範例

julia> @ccall printf("%s = %d\n"::Cstring ; "foo"::Cstring, foo::Cint)::Cint
foo = 3
8

ccall 介面

@ccall 還有另一個備用介面。此介面使用上稍不方便,但允許使用者指定 呼叫慣例

ccall 的參數為

  1. (:function, "library") 配對(最常見),

    :function 函數名稱符號或 "function" 函數名稱字串(用於目前處理程序或 libc 中的符號),

    函數指標(例如,來自 dlsym)。

  2. 函數的傳回類型

  3. 與函數簽章對應的輸入類型組。一個常見的錯誤是忘記 1 組參數類型必須以尾隨逗號撰寫。

  4. 要傳遞給函數的實際參數值(如果有);每個都是個別參數。

注意

(:function, "library") 配對、傳回類型和輸入類型必須是文字常數(亦即,它們不能是變數,但請參閱 非常數函數規格)。

其餘參數會在編譯時間評估,也就是在定義包含方法時。

巨集和函數介面之間的轉換對照表如下。

@ccallccall
@ccall clock()::Int32ccall(:clock, Int32, ())
@ccall f(a::Cint)::Cintccall(:a, Cint, (Cint,), a)
@ccall "mylib".f(a::Cint, b::Cdouble)::Cvoidccall((:f, "mylib"), Cvoid, (Cint, Cdouble), (a, b))
@ccall $fptr.f()::Cvoidccall(fptr, f, Cvoid, ())
@ccall printf("%s = %d\n"::Cstring ; "foo"::Cstring, foo::Cint)::Cint<不可用>
@ccall printf("%s = %d\n"::Cstring ; "2 + 2"::Cstring, "5"::Cstring)::Cintccall(:printf, Cint, (Cstring, Cstring...), "%s = %s\n", "2 + 2", "5")
<不可用>ccall(:gethostname, stdcall, Int32, (Ptr{UInt8}, UInt32), hn, length(hn))

呼叫慣例

ccall 的第二個參數(緊接在回傳類型之前)可以選擇性地作為呼叫慣例指定符(@ccall 巨集目前不支援提供呼叫慣例)。如果沒有任何指定符,則使用平台預設的 C 呼叫慣例。其他支援的慣例包括:stdcallcdeclfastcallthiscall(在 64 位元 Windows 上為無作用)。例如(取自 base/libc.jl),我們看到與上述相同的 gethostnameccall,但具有正確的 Windows 簽章

hn = Vector{UInt8}(undef, 256)
err = ccall(:gethostname, stdcall, Int32, (Ptr{UInt8}, UInt32), hn, length(hn))

如需更多資訊,請參閱 LLVM 語言參考

有一個額外的特殊呼叫慣例 llvmcall,它允許直接插入對 LLVM 內聯函式的呼叫。當針對不尋常的平台(例如 GPGPU)時,這可能特別有用。例如,對於 CUDA,我們需要能夠讀取執行緒索引

ccall("llvm.nvvm.read.ptx.sreg.tid.x", llvmcall, Int32, ())

與任何 ccall 一樣,取得正確的參數簽章至關重要。此外,請注意,與 Core.Intrinsics 公開的等效 Julia 函式不同,沒有相容性層可以確保內聯函式有意義且可在目前的目標上執行。

存取全域變數

原生函式庫匯出的全域變數可以使用 cglobal 函式透過名稱存取。 cglobal 的參數是與 ccall 使用相同的符號規格,以及描述儲存在變數中的值的類型

julia> cglobal((:errno, :libc), Int32)
Ptr{Int32} @0x00007f418d0816b8

結果是一個指標,提供該值的位址。可以使用 unsafe_loadunsafe_store! 透過這個指標來操作該值。

注意

這個 errno 符號可能不會在名為「libc」的函式庫中找到,因為這是系統編譯器的實作細節。一般來說,應該只透過名稱存取標準函式庫符號,讓編譯器填入正確的符號。不過,這個範例中顯示的 errno 符號在大部分編譯器中都是特殊的,因此這裡看到的數值可能不是您預期或想要的。在任何支援多執行緒的系統上編譯等效的 C 程式碼,通常會實際呼叫不同的函式(透過巨集前置處理器超載),而且可能產生與這裡列印的舊有數值不同的結果。

透過指標存取資料

以下方法被描述為「不安全」,因為錯誤的指標或類型宣告可能會導致 Julia 突然終止。

給定一個 Ptr{T},類型 T 的內容通常可以使用 unsafe_load(ptr, [index]) 從參照的記憶體複製到 Julia 物件中。索引參數是可選的(預設值為 1),並遵循 Julia 的 1 為基礎的索引慣例。這個函式的行為故意類似於 getindexsetindex!(例如 [] 存取語法)。

傳回值會是一個新的物件,初始化為包含所參考記憶體內容的副本。所參考的記憶體可以安全地釋放或解除。

如果 TAny,則假設記憶體包含對 Julia 物件 (jl_value_t*) 的參考,結果會是對此物件的參考,且物件不會被複製。在這種情況下,您必須小心確保物件始終對垃圾回收器可見(指標不算,但新的參考算),以確保記憶體不會過早被釋放。請注意,如果物件最初不是由 Julia 分配,則新的物件將永遠不會由 Julia 的垃圾回收器終結。如果 Ptr 本身實際上是 jl_value_t*,則可透過 unsafe_pointer_to_objref(ptr) 將其轉換回 Julia 物件參考。(Julia 值 v 可以透過呼叫 pointer_from_objref(v) 轉換為 jl_value_t* 指標,作為 Ptr{Cvoid}。)

反向操作(將資料寫入 Ptr{T}),可以使用 unsafe_store!(ptr, value, [index]) 執行。目前,這僅支援基本型別或其他無指標(isbits)不可變結構型別。

任何會擲回錯誤的運算目前可能尚未實作,且應該張貼為錯誤,以便解決。

如果感興趣的指標是純資料陣列(原始類型或不可變結構),函數 unsafe_wrap(Array, ptr,dims, own = false) 可能更有用。如果 Julia 應該「擁有」底層緩衝區並在返回的 Array 物件完成時呼叫 free(ptr),最後一個參數應為 true。如果省略 own 參數或為 false,呼叫者必須確保緩衝區在所有存取完成前持續存在。

Julia 中 Ptr 類型的算術(例如使用 +)與 C 的指標算術行為不同。在 Julia 中將整數加入 Ptr 始終會將指標移動一些位元組,而不是元素。這樣,從指標算術取得的位址值就不會依賴指標的元素類型。

執行緒安全性

某些 C 函式庫會從不同執行緒執行其回呼,而且由於 Julia 不是執行緒安全的,因此您需要採取一些額外的預防措施。特別是,您需要設定一個兩層系統:C 回呼應該只透過 Julia 的事件迴圈排程「實際」回呼的執行。為此,請建立一個 AsyncCondition 物件並在它上面等待

cond = Base.AsyncCondition()
wait(cond)

您傳遞給 C 的回呼應該只執行一個 ccall:uv_async_send,傳遞 cond.handle 作為引數,並小心避免任何配置或與 Julia 執行時期的其他互動。

請注意事件可能會合併,因此多次呼叫 uv_async_send 可能會導致單一喚醒通知至條件。

更多關於回呼

如需如何將回呼傳遞至 C 函式庫的更多詳細資訊,請參閱此 部落格文章

C++

如需建立 C++ 繫結的工具,請參閱 CxxWrap 套件。

  • 1C 和 Julia 中的非函式庫呼叫可以內嵌,因此可能比呼叫共用函式庫函式有更少的開銷。上述重點是實際執行外部函式呼叫的成本與在任一原生語言中執行呼叫的成本大約相同。
  • 2Clang 套件 可用於從 C 標頭檔自動產生 Julia 程式碼。