Calling C and Fortran Code

على الرغم من أنه يمكن كتابة معظم الشيفرات بلغة جوليا، إلا أن هناك العديد من المكتبات عالية الجودة والناضجة للحوسبة العددية التي تم كتابتها بالفعل بلغة C وFortran. للسماح بالاستخدام السهل لهذه الشيفرات الموجودة، تجعل جوليا من السهل والفعال استدعاء دوال C وFortran. تتمتع جوليا بفلسفة "عدم وجود كود إضافي": يمكن استدعاء الدوال مباشرة من جوليا دون أي كود "لاصق"، أو توليد كود، أو تجميع - حتى من موجه الأوامر التفاعلي. يتم تحقيق ذلك ببساطة من خلال إجراء استدعاء مناسب باستخدام ماكرو @ccall (أو الصيغة الأقل ملاءمة ccall، انظر ccall syntax section).

يجب أن يكون الكود الذي سيتم استدعاؤه متاحًا كمكتبة مشتركة. معظم مكتبات C و Fortran تُشحن مُجمعة كمكتبات مشتركة بالفعل، ولكن إذا كنت تقوم بتجميع الكود بنفسك باستخدام GCC (أو Clang)، فستحتاج إلى استخدام خيارات -shared و -fPIC. التعليمات البرمجية التي تولدها JIT الخاصة بـ Julia هي نفسها كما لو كانت استدعاءً محليًا لـ C، لذا فإن التكلفة الناتجة هي نفسها كما هو الحال عند استدعاء دالة مكتبة من كود C. [1]

بشكل افتراضي، تقوم مترجمات Fortran generate mangled names (على سبيل المثال، تحويل أسماء الدوال إلى أحرف صغيرة أو كبيرة، وغالبًا ما يتم إضافة شرطة سفلية)، وبالتالي لاستدعاء دالة 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 هو ثابت أو حرف نصي (لكن انظر Non-constant Function Specifications أدناه). يمكن حذف المكتبة، وفي هذه الحالة يتم حل اسم الدالة في العملية الحالية. يمكن استخدام هذا الشكل لاستدعاء دوال مكتبة C، أو دوال في وقت تشغيل جوليا، أو دوال في تطبيق مرتبط بجوليا. يمكن أيضًا تحديد المسار الكامل إلى المكتبة. بدلاً من ذلك، يمكن أيضًا استخدام @ccall لاستدعاء مؤشر دالة $function_pointer، مثل الذي يتم إرجاعه بواسطة Libdl.dlsym. تتوافق argtypes مع توقيع دالة C و argvalues هي القيم الفعلية للوسائط التي سيتم تمريرها إلى الدالة.

Note

انظر أدناه لكيفية map C types to Julia types.

كمثال كامل ولكنه بسيط، تستدعي التعليمات التالية دالة clock من مكتبة C القياسية على معظم أنظمة Unix المشتقة:

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 معروفة بعدم اتساقها في كيفية الإشارة إلى حالات الخطأ. على سبيل المثال، يتم لف دالة مكتبة 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 لملء المصفوفة باسم المضيف. أخيرًا، يأخذ مؤشرًا إلى مخزن اسم المضيف، ويحول المؤشر إلى سلسلة جوليا، على افتراض أنها سلسلة C منتهية بـ null.

من الشائع أن تستخدم مكتبات C هذا النمط الذي يتطلب من المتصل تخصيص الذاكرة ليتم تمريرها إلى المتلقي وملؤها. يتم عادةً تخصيص الذاكرة من جوليا بهذه الطريقة عن طريق إنشاء مصفوفة غير مهيأة وتمرير مؤشر إلى بياناتها إلى دالة C. لهذا السبب لا نستخدم نوع Cstring هنا: حيث أن المصفوفة غير مهيأة، فقد تحتوي على بايتات فارغة. التحويل إلى Cstring كجزء من @ccall يتحقق من وجود بايتات فارغة وبالتالي قد يؤدي إلى حدوث خطأ في التحويل.

إلغاء مرجعية pointer(hostname) باستخدام unsafe_string هو عملية غير آمنة لأنها تتطلب الوصول إلى الذاكرة المخصصة لـ 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.

Creating C-Compatible Julia Function Pointers

من الممكن تمرير دوال جوليا إلى دوال C الأصلية التي تقبل وسائط مؤشرات الدوال. على سبيل المثال، لمطابقة بروتوكولات C بالشكل التالي:

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

الماكرو @cfunction يولد مؤشر دالة متوافق مع C لاستدعاء دالة جوليا. المعاملات لـ 4d61726b646f776e2e436f64652822222c2022406366756e6374696f6e2229_40726566 هي:

  1. دالة جوليا
  2. نوع إرجاع الدالة
  3. زوج من أنواع الإدخال، يتوافق مع توقيع الدالة
Note

كما هو الحال مع @ccall، يجب أن تكون نوع الإرجاع وأنواع الإدخال ثوابت حرفية.

Note

حاليًا، يتم دعم فقط اتفاقية استدعاء C الافتراضية على المنصة. هذا يعني أن المؤشرات التي تم إنشاؤها بواسطة @cfunction لا يمكن استخدامها في الاستدعاءات حيث يتوقع WINAPI وظيفة stdcall على نظام Windows 32 بت، ولكن يمكن استخدامها على WIN64 (حيث يتم توحيد stdcall مع اتفاقية استدعاء C).

Note

يجب ألا تقوم دوال الاستدعاء المرتجعة عبر @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 (أو صفر إذا كان أي ترتيب مسموحًا به).

الآن، افترض أن لدينا مصفوفة أحادية البعد A من القيم في جوليا التي نريد فرزها باستخدام دالة qsort (بدلاً من دالة sort المدمجة في جوليا). قبل أن نفكر في استدعاء qsort وتمرير المعاملات، نحتاج إلى كتابة دالة مقارنة:

julia> function mycompare(a, b)::Cint
           return (a < b) ? -1 : ((a > b) ? +1 : 0)
       end;

qsort يتوقع دالة مقارنة تعيد int في C، لذا نقوم بتعليق نوع الإرجاع ليكون Cint.

لتمرير هذه الدالة إلى C، نحصل على عنوانها باستخدام الماكرو @cfunction:

julia> mycompare_c = @cfunction(mycompare, Cint, (Ref{Cdouble}, Ref{Cdouble}));

@cfunction يتطلب ثلاثة معطيات: دالة جوليا (mycompare)، نوع الإرجاع (Cint)، و tuple حرفي لأنواع معطيات الإدخال، في هذه الحالة لفرز مصفوفة من عناصر 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]. لاحظ أن جوليا takes care of converting the array to a Ptr{Cdouble})، حساب حجم نوع العنصر بالبايت، وما إلى ذلك.

للمتعة، حاول إدراج سطر println("mycompare($a, $b)") في mycompare، والذي سيسمح لك برؤية المقارنات التي يقوم بها qsort (وللتحقق من أنه يستدعي فعلاً دالة جوليا التي قمت بتمريرها إليه).

Mapping C Types to Julia

من الضروري مطابقة نوع C المعلن عنه بدقة مع إعلانه في جوليا. يمكن أن تتسبب الت inconsistencies في حدوث أخطاء في الشيفرة التي تعمل بشكل صحيح على نظام واحد في الفشل أو إنتاج نتائج غير محددة على نظام مختلف.

لاحظ أنه لا يتم استخدام أي ملفات رأس 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. على سبيل المثال، يتم استخدام هذا لتحويل Array من الكائنات (مثل السلاسل النصية) إلى مصفوفة من المؤشرات.

Base.unsafe_convert يتعامل مع التحويل إلى Ptr أنواع. يُعتبر غير آمن لأن تحويل كائن إلى مؤشر أصلي يمكن أن يخفي الكائن عن جامع القمامة، مما يتسبب في تحريره بشكل مبكر.

Type Correspondences

أولاً، دعونا نستعرض بعض المصطلحات المتعلقة بنوع جوليا:

Syntax / KeywordExampleDescription
mutable structBitSet"Leaf Type" :: A group of related data that includes a type-tag, is managed by the Julia GC, and is defined by object-identity. The type parameters of a leaf type must be fully defined (no TypeVars are allowed) in order for the instance to be constructed.
abstract typeAny, AbstractArray{T, N}, Complex{T}"Super Type" :: A super-type (not a leaf-type) that cannot be instantiated, but can be used to describe a group of types.
T{A}Vector{Int}"Type Parameter" :: A specialization of a type (typically used for dispatch or storage optimization).
"TypeVar" :: The T in the type parameter declaration is referred to as a TypeVar (short for type variable).
primitive typeInt, Float64"Primitive Type" :: A type with no fields, but a size. It is stored and defined by-value.
structPair{Int, Int}"Struct" :: A type with all fields defined to be constant. It is defined by-value, and may be stored with a type-tag.
ComplexF64 (isbits)"Is-Bits" :: A primitive type, or a struct type where all fields are other isbits types. It is defined by-value, and is stored without a type-tag.
struct ...; endnothing"Singleton" :: a Leaf Type or Struct with no fields.
(...) or tuple(...)(1, 2, 3)"Tuple" :: an immutable data-structure similar to an anonymous struct type, or a constant array. Represented as either an array or a struct.

Bits Types

هناك عدة أنواع خاصة يجب أن تكون على دراية بها، حيث لا يمكن تعريف أي نوع آخر ليعمل بنفس الطريقة:

  • Float32

    يتوافق تمامًا مع نوع float في C (أو REAL*4 في Fortran).

  • Float64

    يتوافق تمامًا مع نوع double في C (أو REAL*8 في Fortran).

  • ComplexF32

    يتوافق تمامًا مع نوع complex float في C (أو COMPLEX*8 في Fortran).

  • ComplexF64

    يتوافق تمامًا مع نوع complex double في C (أو COMPLEX*16 في Fortran).

  • موقع

    يتوافق تمامًا مع التوضيح النوعي signed في C (أو أي نوع INTEGER في Fortran). يُفترض أن أي نوع في جوليا ليس من نوع فرعي لـ Signed هو نوع غير موقع.

  • Ref{T}

    يتصرف مثل Ptr{T} الذي يمكنه إدارة ذاكرته عبر جامع القمامة في جوليا.

  • Array{T,N}

    عندما يتم تمرير مصفوفة إلى C كوسيط Ptr{T}، لا يتم إعادة تفسيرها: تتطلب جوليا أن يتطابق نوع عنصر المصفوفة مع T، ويتم تمرير عنوان العنصر الأول.

    لذلك، إذا كان Array يحتوي على بيانات بتنسيق خاطئ، فسيتعين تحويله بشكل صريح باستخدام استدعاء مثل trunc.(Int32, A).

    لتمرير مصفوفة A كمؤشر من نوع مختلف دون تحويل البيانات مسبقًا (على سبيل المثال، لتمرير مصفوفة Float64 إلى دالة تعمل على بايتات غير مفسرة)، يمكنك إعلان الوسيطة كـ Ptr{Cvoid}.

    إذا تم تمرير مصفوفة من نوع Ptr{T} كوسيط من نوع Ptr{Ptr{T}}، فسيحاول Base.cconvert أولاً إنشاء نسخة من المصفوفة منتهية بـ null مع استبدال كل عنصر بنسخته 4d61726b646f776e2e436f64652822222c2022426173652e63636f6e766572742229_40726566. وهذا يسمح، على سبيل المثال، بتمرير مصفوفة مؤشرات argv من نوع Vector{String} إلى وسيط من نوع Ptr{Ptr{Cchar}}.

على جميع الأنظمة التي ندعمها حاليًا، يمكن ترجمة أنواع القيم الأساسية في C/C++ إلى أنواع جوليا كما يلي. كل نوع C له نوع جوليا المقابل بنفس الاسم، مسبوقًا بـ C. يمكن أن يساعد ذلك عند كتابة كود محمول (وتذكر أن int في C ليس هو نفسه Int في جوليا).

أنواع مستقلة عن النظام

C nameFortran nameStandard Julia AliasJulia Base Type
unsigned charCHARACTERCucharUInt8
bool (_Bool in C99+)CucharUInt8
shortINTEGER*2, LOGICAL*2CshortInt16
unsigned shortCushortUInt16
int, BOOL (C, typical)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 and [[noreturn]] or _NoreturnUnion{}
void*Ptr{Cvoid} (or similarly Ref{Cvoid})
T* (where T represents an appropriately defined type)Ref{T} (T may be safely mutated only if T is an isbits type)
char* (or char[], e.g. a string)CHARACTER*NCstring if null-terminated, or Ptr{UInt8} if not
char** (or *char[])Ptr{Ptr{UInt8}}
jl_value_t* (any Julia Type)Any
jl_value_t* const* (a reference to a Julia value)Ref{Any} (const, since mutation would require a write barrier, which is not possible to insert correctly)
va_argNot supported
... (variadic function specification)T... (where T is one of the above types, when using the ccall function)
... (variadic function specification); va_arg1::T, va_arg2::S, etc. (only supported with @ccall macro)

نوع Cstring هو في الأساس مرادف لـ Ptr{UInt8}، باستثناء أن التحويل إلى Cstring يرمي خطأ إذا كانت سلسلة جوليا تحتوي على أي أحرف null مضمنة (والتي ستؤدي إلى تقصير السلسلة بصمت إذا كانت الروتين C تعالج null كمنهي). إذا كنت تمرر char* إلى روتين C لا يفترض إنهاء null (على سبيل المثال، لأنك تمرر طول السلسلة بشكل صريح)، أو إذا كنت تعرف بالتأكيد أن سلسلة جوليا الخاصة بك لا تحتوي على null وترغب في تخطي الفحص، يمكنك استخدام Ptr{UInt8} كنوع وسيط. يمكن أيضًا استخدام Cstring كنوع إرجاع ccall، ولكن في هذه الحالة من الواضح أنها لا تضيف أي فحوصات إضافية وتهدف فقط إلى تحسين قابلية قراءة الاستدعاء.

أنواع تعتمد على النظام

C nameStandard Julia AliasJulia Base Type
charCcharInt8 (x86, x86_64), UInt8 (powerpc, arm)
longClongInt (UNIX), Int32 (Windows)
unsigned longCulongUInt (UNIX), UInt32 (Windows)
wchar_tCwchar_tInt32 (UNIX), UInt16 (Windows)
Note

عند استدعاء Fortran، يجب تمرير جميع المدخلات بواسطة مؤشرات إلى قيم مخصصة في الذاكرة (heap) أو مخصصة في المكدس (stack)، لذا يجب أن تحتوي جميع المطابقات النوعية المذكورة أعلاه على غلاف إضافي Ptr{..} أو Ref{..} حول مواصفات نوعها.

Warning

بالنسبة لوسائط السلسلة (char*)، يجب أن يكون نوع جوليا هو Cstring (إذا كان من المتوقع وجود بيانات منتهية بـ null)، أو إما Ptr{Cchar} أو Ptr{UInt8} خلاف ذلك (هذان النوعان من المؤشرات لهما نفس التأثير)، كما هو موضح أعلاه، وليس String. وبالمثل، بالنسبة لوسائط المصفوفة (T[] أو T*)، يجب أن يكون نوع جوليا مرة أخرى Ptr{T}، وليس Vector{T}.

Warning

نوع Char في جوليا هو 32 بت، وهو ليس هو نفس نوع الحرف العريض (wchar_t أو wint_t) على جميع المنصات.

Warning

نوع الإرجاع Union{} يعني أن الدالة لن ترجع، أي ما يعادل [[noreturn]] في C++11 أو _Noreturn في C11 (مثل jl_throw أو longjmp). لا تستخدم هذا للوظائف التي لا ترجع قيمة (void) ولكنها ترجع، بالنسبة لتلك، استخدم Cvoid بدلاً من ذلك.

Note

بالنسبة لوسائط wchar_t*، يجب أن يكون نوع جوليا Cwstring (إذا كانت الروتين C تتوقع سلسلة منتهية بـ null)، أو Ptr{Cwchar_t} بخلاف ذلك. لاحظ أيضًا أن بيانات سلسلة UTF-8 في جوليا منتهية داخليًا بـ null، لذا يمكن تمريرها إلى دوال C التي تتوقع بيانات منتهية بـ null دون الحاجة إلى عمل نسخة (لكن استخدام نوع Cwstring سيتسبب في حدوث خطأ إذا كانت السلسلة نفسها تحتوي على أحرف null).

Note

يمكن استدعاء دوال C التي تأخذ وسيطًا من نوع char** باستخدام نوع 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
Note

بالنسبة لدوال Fortran التي تأخذ سلاسل نصية ذات طول متغير من النوع character(len=*)، يتم توفير أطوال السلاسل كـ معاملات مخفية. نوع وموقع هذه المعاملات في القائمة محدد من قبل المترجم، حيث يقوم بائعي المترجمين عادةً باستخدام 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))
Warning

قد تضيف مترجمات Fortran قد أيضًا معلمات مخفية أخرى للمؤشرات، والمصفوفات ذات الشكل المفترض (:) والمصفوفات ذات الحجم المفترض (*). يمكن تجنب هذا السلوك باستخدام ISO_C_BINDING وإدراج bind(c) في تعريف الروتين الفرعي، وهو موصى به بشدة للشفرة القابلة للتشغيل المتداخل. في هذه الحالة، لن تكون هناك أي معلمات مخفية، على حساب بعض ميزات اللغة (على سبيل المثال، سيتم السماح فقط بـ character(len=1) لتمرير السلاسل النصية).

Note

سترجع دالة C المعلنة لإرجاع Cvoid القيمة nothing في جوليا.

Struct Type Correspondences

يمكن عكس الأنواع المركبة مثل struct في C أو TYPE في Fortran90 (أو STRUCTURE / RECORD في بعض متغيرات F77) في جوليا عن طريق إنشاء تعريف struct بنفس تخطيط الحقول.

عند استخدام isbits بشكل متكرر، يتم تخزين الأنواع داخل السطر. يتم تخزين جميع الأنواع الأخرى كمؤشر إلى البيانات. عند عكس هيكل مستخدم بالقيمة داخل هيكل آخر في C، من الضروري ألا تحاول نسخ الحقول يدويًا، حيث إن ذلك لن يحافظ على محاذاة الحقول الصحيحة. بدلاً من ذلك، قم بإعلان نوع هيكل isbits واستخدمه بدلاً من ذلك. الهياكل غير المسماة غير ممكنة في الترجمة إلى جوليا.

لا تدعم جوليًا إعلانات الهياكل والاتحادات المعبأة.

يمكنك الحصول على تقريب لـ union إذا كنت تعرف، مسبقًا، الحقل الذي سيكون له أكبر حجم (قد يشمل الحشو). عند ترجمة حقولك إلى جوليا، قم بإعلان الحقل في جوليا ليكون من هذا النوع فقط.

يمكن التعبير عن مصفوفات المعلمات باستخدام NTuple. على سبيل المثال، يتم كتابة الهيكل في تدوين C كالتالي

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)

المصفوفات ذات الحجم غير المعروف (الهياكل ذات الطول المتغير المتوافقة مع C99 المحددة بواسطة [] أو [0]) غير مدعومة مباشرة. غالبًا ما تكون أفضل طريقة للتعامل مع هذه هي التعامل مع إزاحات البايت مباشرة. على سبيل المثال، إذا كانت مكتبة 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 ليست لغة ديناميكية مثل Julia، فإن دوالها يمكن أن تقبل فقط أنواع المعاملات ذات التوقيع الثابت المعروف مسبقًا.

ومع ذلك، بينما يجب أن يكون تخطيط النوع معروفًا بشكل ثابت لحساب ABI المقصود لـ C، تعتبر المعلمات الثابتة للدالة جزءًا من هذا البيئة الثابتة. يمكن استخدام المعلمات الثابتة للدالة كمعلمات نوع في توقيع الاستدعاء، طالما أنها لا تؤثر على تخطيط النوع. على سبيل المثال، 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 Values

إذا كانت روتين C/C++ يحتوي على وسيط أو قيمة إرجاع من نوع SIMD أصلي، فإن نوع جوليا المقابل هو مجموعة متجانسة من VecElement التي تتوافق بشكل طبيعي مع نوع SIMD. على وجه التحديد:

  • يجب أن تكون المجموعة بنفس حجم وعناصر نوع SIMD. على سبيل المثال، يجب أن تحتوي مجموعة تمثل __m128 على x86 على حجم 16 بايت وعناصر Float32.
  • يجب أن يكون نوع عنصر التوابل مثيلًا لـ VecElement{T} حيث أن T هو نوع بدائي بحجم عدد من البايتات يكون قوة للعدد 2 (مثل 1، 2، 4، 8، 16، إلخ) مثل Int8 أو Float64.

على سبيل المثال، اعتبر هذه الروتين C الذي يستخدم دوال AVX:

#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)));
}

الكود التالي في جوليا يستدعي dist باستخدام ccall:

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 في جوليا، حيث قد يؤدي ذلك إلى استدعاء دالة free عبر المكتبة الخاطئة ويتسبب في إنهاء العملية. العكس (تمرير كائن تم تخصيصه في جوليا ليتم تحريره بواسطة مكتبة خارجية) غير صالح بنفس القدر.

When to use T, Ptr{T} and Ref{T}

في كود جوليا الذي يلتف حول استدعاءات روتين C الخارجي، يجب أن يتم إعلان البيانات العادية (غير المؤشر) من النوع T داخل @ccall، حيث يتم تمريرها بالقيمة. بالنسبة لكود C الذي يقبل المؤشرات، يجب عمومًا استخدام Ref{T} لأنواع المعاملات المدخلة، مما يسمح باستخدام مؤشرات للذاكرة المدارة إما بواسطة جوليا أو C من خلال الاستدعاء الضمني إلى Base.cconvert. في المقابل، يجب أن يتم إعلان المؤشرات التي تعيدها دالة C المستدعاة من النوع Ptr{T}، مما يعكس أن الذاكرة المشار إليها تُدار فقط بواسطة C. يجب تمثيل المؤشرات الموجودة في هياكل C كحقول من النوع Ptr{T} داخل أنواع الهياكل جوليا المقابلة المصممة لتقليد الهيكل الداخلي للهياكل C المقابلة.

في كود جوليا الذي يلتف حول استدعاءات روتينات فورتران الخارجية، يجب أن يتم إعلان جميع وسائط الإدخال من نوع Ref{T}، حيث أن فورتران يمرر جميع المتغيرات بواسطة مؤشرات إلى مواقع الذاكرة. يجب أن يكون نوع الإرجاع إما Cvoid لروتينات فورتران الفرعية، أو T لوظائف فورتران التي تعيد النوع T.

Mapping C Functions to Julia

@ccall / @cfunction argument translation guide

لترجمة قائمة وسائط C إلى جوليا:

  • T، حيث T هو أحد الأنواع البدائية: char، int، long، short، float، double، complex، enum أو أي من معادلات typedef الخاصة بها

    • T، حيث T هو نوع بتات جوليا المعادل (وفقًا للجدول أعلاه)
    • إذا كان T هو enum، يجب أن يكون نوع المعامل مكافئًا لـ Cint أو Cuint
    • سيتم نسخ قيمة الوسيطة (تمريرها بالقيمة)
  • struct T (بما في ذلك typedef إلى هيكل)

    • T، حيث T هو نوع ورقة في جوليا
    • سيتم نسخ قيمة الوسيطة (تمريرها بالقيمة)
  • vector T (أو __attribute__ vector_size، أو typedef مثل __m128)

    • NTuple{N, VecElement{T}}، حيث T هو نوع جوليّا بدائي بالحجم الصحيح وN هو عدد العناصر في المتجه (يساوي vector_size / sizeof T).
  • void*

    • يعتمد ذلك على كيفية استخدام هذا المعامل، أولاً ترجم هذا إلى نوع المؤشر المقصود، ثم حدد المعادل في جوليا باستخدام القواعد المتبقية في هذه القائمة
    • يمكن إعلان هذه الحجة كـ Ptr{Cvoid} إذا كانت حقًا مجرد مؤشر غير معروف
  • jl_value_t*

    • أي
    • قيمة الوسيطة يجب أن تكون كائن جوليا صالح
  • jl_value_t* const*

    • Ref{Any}
    • قائمة الوسائط يجب أن تكون كائن جوليا صالح (أو C_NULL)
    • لا يمكن استخدامه كمعامل إخراج، ما لم يتمكن المستخدم من ترتيب الحفاظ على كائن GC بشكل منفصل.
  • T*

    • Ref{T}، حيث T هو نوع جوليا المقابل لـ T
    • سيتم نسخ قيمة الوسيطة إذا كانت من نوع inlinealloc (الذي يتضمن isbits بخلاف ذلك، يجب أن تكون القيمة كائن جوليا صالح.
  • T (*)(...) (على سبيل المثال، مؤشر إلى دالة)

    • Ptr{Cvoid} (قد تحتاج إلى استخدام @cfunction بشكل صريح لإنشاء هذه المؤشر)
  • ... (على سبيل المثال، vararg)

    • [for ccall]: T...، حيث T هو نوع جوليا الفردي لجميع المعاملات المتبقية
    • [for @ccall]: ; va_arg1::T, va_arg2::S, etc، حيث T و S هما نوع جوليا (أي فصل المعاملات العادية عن varargs باستخدام ;)
    • غير مدعوم حاليًا بواسطة @cfunction
  • va_arg

    • غير مدعوم بواسطة ccall أو @cfunction

@ccall / @cfunction return type translation guide

لترجمة نوع إرجاع C إلى جوليا:

  • فارغ

    • Cvoid (سيعيد مثيل الساكن nothing::Cvoid)
  • T، حيث T هو أحد الأنواع البدائية: char، int، long، short، float، double، complex، enum أو أي من معادلات typedef الخاصة بهم

    • نفس قائمة وسائط C
    • سيتم نسخ قيمة الوسيطة (تم إرجاعها بالقيمة)
  • struct T (بما في ذلك typedef إلى هيكل)

    • نفس قائمة وسائط C
    • سيتم نسخ قيمة الوسيطة (إرجاعها بالقيمة)
  • vector T

    • نفس قائمة وسائط C
  • void*

    • يعتمد ذلك على كيفية استخدام هذا المعامل، أولاً ترجم هذا إلى نوع المؤشر المقصود، ثم حدد المعادل في جوليا باستخدام القواعد المتبقية في هذه القائمة
    • يمكن إعلان هذه الحجة كـ Ptr{Cvoid} إذا كانت حقًا مجرد مؤشر غير معروف
  • jl_value_t*

    • أي
    • قيمة الوسيطة يجب أن تكون كائن جوليا صالح
  • jl_value_t**

    • Ptr{Any} (Ref{Any} غير صالح كنوع إرجاع)
  • T*

    • إذا كانت الذاكرة مملوكة بالفعل بواسطة جوليا، أو كانت من نوع isbits، ومعروفة بأنها غير فارغة:

      • Ref{T}، حيث T هو نوع جوليا المقابل لـ T
      • نوع الإرجاع Ref{Any} غير صالح، يجب أن يكون إما Any (المقابل لـ jl_value_t*) أو Ptr{Any} (المقابل لـ jl_value_t**)
      • C يجب ألا يعدل الذاكرة المعادة عبر Ref{T} إذا كان T من نوع isbits
    • إذا كانت الذاكرة مملوكة لـ C:

      • Ptr{T}، حيث T هو نوع جوليا المقابل لـ T
  • T (*)(...) (على سبيل المثال، مؤشر إلى دالة)

    • Ptr{Cvoid} لاستدعاء هذا مباشرة من جوليا، ستحتاج إلى تمرير هذا كأول وسيط إلى @ccall. انظر Indirect Calls.

Passing Pointers for Modifying Inputs

لأن C لا تدعم القيم المرجعة المتعددة، غالبًا ما تأخذ دوال C مؤشرات إلى بيانات ستقوم الدالة بتعديلها. لتحقيق ذلك ضمن @ccall، تحتاج أولاً إلى encapsulate القيمة داخل 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[]؛ أي أنها تعمل مثل مصفوفات ذات أبعاد صفرية.

C Wrapper Examples

لنبدأ بمثال بسيط لملف تغليف C الذي يعيد نوع Ptr:

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 Scientific Library (هنا يُفترض أن يكون متاحًا من خلال :libgsl) مؤشراً غير شفاف، gsl_permutation *، كنوع إرجاع لدالة C gsl_permutation_alloc. حيث أن كود المستخدم لا يحتاج أبدًا للنظر داخل بنية gsl_permutation، فإن التغليف المقابل في جوليا يحتاج ببساطة إلى إعلان نوع جديد، gsl_permutation، الذي ليس له حقول داخلية والغرض الوحيد منه هو أن يتم وضعه في معلمة النوع لنوع Ptr. يتم إعلان نوع الإرجاع لـ ccall كـ Ptr{gsl_permutation}، حيث أن الذاكرة المخصصة والمشار إليها بواسطة output_ptr يتم التحكم فيها بواسطة C.

يتم تمرير الإدخال n بالقيمة، لذا فإن توقيع إدخال الدالة يُعلن ببساطة كـ ::Csize_t دون الحاجة إلى أي Ref أو Ptr. (إذا كانت اللفافة تستدعي دالة 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

إليك مثال ثالث يوضح تمرير مصفوفات جوليا:

# 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 الملتفة بإرجاع رمز خطأ صحيح؛ نتائج التقييم الفعلي لدالة بيسل J تملأ مصفوفة جوليا result_array. يتم إعلان هذه المتغير كـ Ref{Cdouble}، حيث يتم تخصيص وإدارة ذاكرته بواسطة جوليا. يستدعي الاتصال الضمني Base.cconvert(Ref{Cdouble}, result_array) مؤشرات جوليا إلى بنية بيانات مصفوفة جوليا في شكل مفهوم من قبل C.

Fortran Wrapper Example

المثال التالي يستخدم ccall لاستدعاء دالة في مكتبة Fortran شائعة (libBLAS) لحساب حاصل الضرب النقطي. لاحظ أن تخطيط المعاملات هنا مختلف قليلاً عن السابق، حيث نحتاج إلى التخطيط من جوليا إلى 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 ستخزن مرجعًا إلى الذاكرة المخصصة بواسطة جوليا، بعد أن يعود @ccall، يجب عليك التأكد من أن الكائن يظل مرئيًا لجمع القمامة. الطريقة المقترحة للقيام بذلك هي جعل متغير عالمي من النوع Array{Ref,1} للاحتفاظ بهذه القيم حتى تخبرك مكتبة C أنها انتهت منها.

كلما قمت بإنشاء مؤشر لبيانات جوليا، يجب عليك التأكد من أن البيانات الأصلية موجودة حتى تنتهي من استخدام المؤشر. العديد من الطرق في جوليا مثل unsafe_load و String تقوم بعمل نسخ من البيانات بدلاً من أخذ ملكية المخزن، بحيث يكون من الآمن تحرير (أو تغيير) البيانات الأصلية دون التأثير على جوليا. استثناء ملحوظ هو unsafe_wrap الذي، لأسباب تتعلق بالأداء، يشارك (أو يمكن إخبارها بأخذ ملكية) المخزن الأساسي.

لا يضمن جامع القمامة أي ترتيب للتنفيذ النهائي. أي أنه إذا كانت a تحتوي على مرجع إلى b وكان كل من a و b مستحقين لجمع القمامة، فلا يوجد ضمان بأن b سيتم إنهاؤه بعد a. إذا كان التنفيذ النهائي الصحيح لـ a يعتمد على كون b صالحًا، يجب التعامل مع ذلك بطرق أخرى.

Non-constant Function Specifications

في بعض الحالات، لا يُعرف الاسم الدقيق أو مسار المكتبة المطلوبة مسبقًا ويجب حسابه في وقت التشغيل. للتعامل مع مثل هذه الحالات، يمكن أن يكون تحديد مكون المكتبة عبارة عن استدعاء دالة، مثل find_blas().dgemm. سيتم تنفيذ تعبير الاستدعاء عندما يتم تنفيذ ccall نفسه. ومع ذلك، يُفترض أن موقع المكتبة لا يتغير بمجرد تحديده، لذا يمكن تخزين نتيجة الاستدعاء وإعادة استخدامها. لذلك، فإن عدد مرات تنفيذ التعبير غير محدد، وإرجاع قيم مختلفة لاستدعاءات متعددة يؤدي إلى سلوك غير محدد.

إذا كانت هناك حاجة إلى مزيد من المرونة، فمن الممكن استخدام القيم المحسوبة كأسماء دوال من خلال التدرج عبر eval كما يلي:

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

هذا التعبير ينشئ اسمًا باستخدام string، ثم يستبدل هذا الاسم في تعبير @ccall جديد، والذي يتم تقييمه بعد ذلك. ضع في اعتبارك أن eval يعمل فقط على المستوى الأعلى، لذا ضمن هذا التعبير لن تكون المتغيرات المحلية متاحة (ما لم يتم استبدال قيمها بـ $). لهذا السبب، يتم استخدام eval عادةً فقط لتشكيل التعريفات على المستوى الأعلى، على سبيل المثال عند لف المكتبات التي تحتوي على العديد من الوظائف المماثلة. يمكن بناء مثال مشابه لـ @cfunction.

ومع ذلك، فإن القيام بذلك سيكون بطيئًا جدًا وسيسرب الذاكرة، لذا يجب عليك عادةً تجنب ذلك وبدلاً من ذلك الاستمرار في القراءة. يناقش القسم التالي كيفية استخدام المكالمات غير المباشرة لتحقيق تأثير مشابه بكفاءة.

Indirect Calls

الحجة الأولى لـ @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

Closure cfunctions

يمكن تمييز الحجة الأولى لـ @cfunction بعلامة $، وفي هذه الحالة ستكون قيمة الإرجاع بدلاً من ذلك struct CFunction التي تغلق على الحجة. يجب عليك التأكد من أن هذا الكائن المرجع يبقى حياً حتى يتم الانتهاء من جميع استخداماته. سيتم مسح المحتويات والكود عند مؤشر cfunction عبر finalizer عندما يتم إسقاط هذا المرجع وعند الخروج. عادةً لا يكون هذا مطلوبًا، حيث أن هذه الوظيفة غير موجودة في C، ولكن يمكن أن تكون مفيدة للتعامل مع واجهات برمجة التطبيقات ذات التصميم السيئ التي لا توفر معلمة بيئة إغلاق منفصلة.

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
Note

الإغلاق @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 المتغيرة العدد، يمكن استخدام semicolon في قائمة المعاملات لفصل المعاملات المطلوبة عن المعاملات المتغيرة. يتم إعطاء مثال مع دالة printf أدناه:

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

ccall interface

هناك واجهة بديلة أخرى لـ @ccall. هذه الواجهة أقل ملاءمة قليلاً لكنها تسمح بتحديد calling convention.

الحجج إلى ccall هي:

  1. زوج (:function, "library") (الأكثر شيوعًا)،

    أو

    اسم الرمز :function أو سلسلة الاسم "function" (للرموز في العملية الحالية أو libc)،

    أو

    مؤشر دالة (على سبيل المثال، من dlsym).

  2. نوع إرجاع الدالة

  3. زوج من أنواع الإدخال، يتوافق مع توقيع الدالة. واحدة من الأخطاء الشائعة هي نسيان أن 1-زوج من أنواع الوسائط يجب أن يُكتب مع فاصلة متروكة.

  4. القيم الفعلية للوسائط التي سيتم تمريرها إلى الدالة، إن وجدت؛ كل منها هو معلمة منفصلة.

Note

يجب أن تكون مجموعة (:function, "library") ونوع الإرجاع وأنواع الإدخال ثوابت حرفية (أي، لا يمكن أن تكون متغيرات، ولكن انظر إلى Non-constant Function Specifications).

تُقيَّم المعلمات المتبقية في وقت الترجمة، عندما يتم تعريف الطريقة المحتوية.

جدول الترجمات بين واجهات الماكرو والوظائف موضح أدناه.

@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<unavailable>
@ccall printf("%s = %s\n"::Cstring ; "2 + 2"::Cstring, "5"::Cstring)::Cintccall(:printf, Cint, (Cstring, Cstring...), "%s = %s\n", "2 + 2", "5")
<unavailable>ccall(:gethostname, stdcall, Int32, (Ptr{UInt8}, UInt32), hn, length(hn))

Calling Convention

يمكن أن يكون الوسيط الثاني لـ ccall (الذي يسبق نوع الإرجاع مباشرة) محددًا اختياريًا لاتفاقية الاستدعاء (لا يدعم الماكرو @ccall حاليًا إعطاء اتفاقية استدعاء). بدون أي محدد، يتم استخدام اتفاقية الاستدعاء الافتراضية لـ C على النظام الأساسي. الاتفاقيات المدعومة الأخرى هي: stdcall، cdecl، fastcall، و thiscall (لا تعمل على Windows 64 بت). على سبيل المثال (من base/libc.jl) نرى نفس gethostnameccall كما هو مذكور أعلاه، ولكن مع التوقيع الصحيح لـ Windows:

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

للمزيد من المعلومات، يرجى الاطلاع على LLVM Language Reference.

هناك اتفاقية استدعاء خاصة إضافية llvmcall، والتي تسمح بإدراج استدعاءات إلى الدوال الداخلية لـ LLVM مباشرة. يمكن أن يكون هذا مفيدًا بشكل خاص عند استهداف منصات غير عادية مثل GPGPUs. على سبيل المثال، بالنسبة لـ CUDA، نحتاج إلى أن نكون قادرين على قراءة فهرس الخيط:

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

كما هو الحال مع أي ccall، من الضروري الحصول على توقيع المعاملات بشكل دقيق تمامًا. أيضًا، لاحظ أنه لا يوجد طبقة توافق تضمن أن الدالة الداخلية منطقية وتعمل على الهدف الحالي، على عكس وظائف جوليا المعادلة التي تعرضها Core.Intrinsics.

Accessing Global Variables

يمكن الوصول إلى المتغيرات العالمية التي تصدرها المكتبات الأصلية بالاسم باستخدام دالة cglobal. المعاملات لـ 4d61726b646f776e2e436f64652822222c202263676c6f62616c2229_40726566 هي مواصفة رمز مطابقة لتلك المستخدمة في ccall، ونوع يصف القيمة المخزنة في المتغير:

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

النتيجة هي مؤشر يعطي عنوان القيمة. يمكن التلاعب بالقيمة من خلال هذا المؤشر باستخدام unsafe_load و unsafe_store!.

Note

قد لا يتم العثور على هذا الرمز errno في مكتبة تُدعى "libc"، حيث إن هذا يعد تفصيلًا تنفيذيًا لمجمع النظام الخاص بك. عادةً ما يجب الوصول إلى رموز المكتبة القياسية فقط بالاسم، مما يسمح للمجمع بملء الرمز الصحيح. ومع ذلك، فإن الرمز errno المعروض في هذا المثال خاص في معظم المجمعات، لذا فإن القيمة التي تُرى هنا ربما ليست ما تتوقعه أو تريده. عادةً ما يؤدي تجميع الكود المعادل بلغة C على أي نظام قادر على تعدد الخيوط إلى استدعاء دالة مختلفة (عبر تحميل ماكرو المعالج المسبق)، وقد يعطي نتيجة مختلفة عن القيمة القديمة المطبوعة هنا.

Accessing Data through a Pointer

تُوصف الطرق التالية بأنها "غير آمنة" لأن وجود مؤشر خاطئ أو إعلان نوع يمكن أن يتسبب في إنهاء جوليا بشكل مفاجئ.

بالنظر إلى Ptr{T}، يمكن عمومًا نسخ محتويات النوع T من الذاكرة المرجعية إلى كائن جوليا باستخدام unsafe_load(ptr, [index]). حجة الفهرس اختيارية (القيمة الافتراضية هي 1)، وتتبع قاعدة جوليا لفهرسة تبدأ من 1. هذه الوظيفة مشابهة عمدًا لسلوك getindex و setindex! (على سبيل المثال، بناء جملة الوصول []).

ستكون قيمة الإرجاع كائنًا جديدًا مُهيأً ليحتوي على نسخة من محتويات الذاكرة المرجعية. يمكن تحرير الذاكرة المرجعية أو إطلاقها بأمان.

إذا كان T هو Any، فإن الذاكرة يُفترض أن تحتوي على مرجع لكائن جوليا (وهو jl_value_t*)، وستكون النتيجة مرجعًا لهذا الكائن، ولن يتم نسخ الكائن. يجب أن تكون حذرًا في هذه الحالة لضمان أن الكائن كان دائمًا مرئيًا لجمع القمامة (المؤشرات لا تُحتسب، ولكن المرجع الجديد يُحتسب) لضمان عدم تحرير الذاكرة بشكل مبكر. لاحظ أنه إذا لم يكن الكائن قد تم تخصيصه في الأصل بواسطة جوليا، فلن يتم إنهاء الكائن الجديد أبدًا بواسطة جامع القمامة في جوليا. إذا كان Ptr نفسه هو في الواقع jl_value_t*، يمكن تحويله مرة أخرى إلى مرجع كائن جوليا بواسطة unsafe_pointer_to_objref(ptr). (يمكن تحويل قيم جوليا v إلى مؤشرات jl_value_t*، مثل Ptr{Cvoid}، عن طريق استدعاء pointer_from_objref(v).)

يمكن تنفيذ العملية العكسية (كتابة البيانات إلى Ptr{T}) باستخدام unsafe_store!(ptr, value, [index]). حاليًا، يتم دعم ذلك فقط لأنواع البيانات البدائية أو أنواع الهياكل الثابتة غير المرتبطة (isbits) الأخرى.

أي عملية تؤدي إلى خطأ من المحتمل أنها غير منفذة حاليًا ويجب الإبلاغ عنها كخطأ حتى يمكن حلها.

إذا كان مؤشر الاهتمام هو مصفوفة بيانات بسيطة (نوع بدائي أو هيكل غير قابل للتغيير)، فقد تكون الدالة unsafe_wrap(Array, ptr,dims, own = false) أكثر فائدة. يجب أن يكون المعامل النهائي صحيحًا إذا كان يجب على جوليا "تولي ملكية" المخزن الأساسي واستدعاء free(ptr) عند الانتهاء من كائن Array المعاد. إذا تم حذف معامل own أو كان خاطئًا، يجب على المتصل التأكد من بقاء المخزن موجودًا حتى يتم الانتهاء من جميع الوصولات.

العمليات الحسابية على نوع Ptr في جوليا (مثل استخدام +) لا تتصرف بنفس طريقة العمليات الحسابية على المؤشرات في C. إضافة عدد صحيح إلى Ptr في جوليا دائمًا ما تحرك المؤشر بعدد من البايتات، وليس العناصر. بهذه الطريقة، فإن قيم العناوين التي تم الحصول عليها من العمليات الحسابية على المؤشرات لا تعتمد على أنواع العناصر للمؤشرات.

Thread-safety

بعض مكتبات C تنفذ ردودها من خيط مختلف، ونظرًا لأن جوليا ليست آمنة للخيوط، ستحتاج إلى اتخاذ بعض الاحتياطات الإضافية. على وجه الخصوص، ستحتاج إلى إعداد نظام ذو طبقتين: يجب على رد C أن يجدول (عبر حلقة الأحداث في جوليا) تنفيذ ردك "الحقيقي". للقيام بذلك، أنشئ كائن AsyncCondition و wait عليه:

cond = Base.AsyncCondition()
wait(cond)

يجب أن ينفذ الاستدعاء الذي تمرره إلى C فقط ccall إلى :uv_async_send، مع تمرير cond.handle كوسيط، مع الحرص على تجنب أي تخصيصات أو تفاعلات أخرى مع وقت تشغيل جوليا.

لاحظ أن الأحداث قد تتجمع، لذا قد تؤدي عدة استدعاءات لـ uv_async_send إلى إشعار استيقاظ واحد فقط للحالة.

More About Callbacks

لمزيد من التفاصيل حول كيفية تمرير ردود النداء إلى مكتبات C، راجع هذا blog post.

C++

لأدوات إنشاء روابط C++، راجع حزمة CxxWrap.

  • 1Non-library function calls in both C and Julia can be inlined and thus may have even less overhead than calls to shared library functions. The point above is that the cost of actually doing foreign function call is about the same as doing a call in either native language.
  • 2The Clang package can be used to auto-generate Julia code from a C header file.