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
. تتوافق argtype
s مع توقيع دالة C و argvalue
s هي القيم الفعلية للوسائط التي سيتم تمريرها إلى الدالة.
انظر أدناه لكيفية 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
هي:
- دالة جوليا
- نوع إرجاع الدالة
- زوج من أنواع الإدخال، يتوافق مع توقيع الدالة
كما هو الحال مع @ccall
، يجب أن تكون نوع الإرجاع وأنواع الإدخال ثوابت حرفية.
حاليًا، يتم دعم فقط اتفاقية استدعاء C الافتراضية على المنصة. هذا يعني أن المؤشرات التي تم إنشاؤها بواسطة @cfunction
لا يمكن استخدامها في الاستدعاءات حيث يتوقع WINAPI وظيفة stdcall
على نظام Windows 32 بت، ولكن يمكن استخدامها على WIN64 (حيث يتم توحيد stdcall
مع اتفاقية استدعاء C).
يجب ألا تقوم دوال الاستدعاء المرتجعة عبر @cfunction
بإلقاء الأخطاء، حيث إن ذلك سيعيد التحكم إلى وقت تشغيل جوليا بشكل غير متوقع وقد يترك البرنامج في حالة غير معرفة.
مثال كلاسيكي هو دالة مكتبة C القياسية qsort
، المعلنة كالتالي:
void qsort(void *base, size_t nitems, size_t size,
int (*compare)(const void*, const void*));
الوسيط base
هو مؤشر لمصفوفة بطول nitems
، مع عناصر بحجم size
بايت لكل منها. compare
هي دالة رد نداء تأخذ مؤشرات لعنصرين a
و b
وتعيد عددًا صحيحًا أقل/أكبر من الصفر إذا كان يجب أن يظهر a
قبل/بعد b
(أو صفر إذا كان أي ترتيب مسموحًا به).
الآن، افترض أن لدينا مصفوفة أحادية البعد 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 / Keyword | Example | Description |
---|---|---|
mutable struct | BitSet | "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 type | Any , 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 type | Int , Float64 | "Primitive Type" :: A type with no fields, but a size. It is stored and defined by-value. |
struct | Pair{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 ...; end | nothing | "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 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*4i | 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} (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*N | Cstring 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_arg | Not 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 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، يجب تمرير جميع المدخلات بواسطة مؤشرات إلى قيم مخصصة في الذاكرة (heap) أو مخصصة في المكدس (stack)، لذا يجب أن تحتوي جميع المطابقات النوعية المذكورة أعلاه على غلاف إضافي Ptr{..}
أو Ref{..}
حول مواصفات نوعها.
بالنسبة لوسائط السلسلة (char*
)، يجب أن يكون نوع جوليا هو Cstring
(إذا كان من المتوقع وجود بيانات منتهية بـ null)، أو إما Ptr{Cchar}
أو Ptr{UInt8}
خلاف ذلك (هذان النوعان من المؤشرات لهما نفس التأثير)، كما هو موضح أعلاه، وليس String
. وبالمثل، بالنسبة لوسائط المصفوفة (T[]
أو T*
)، يجب أن يكون نوع جوليا مرة أخرى Ptr{T}
، وليس Vector{T}
.
نوع Char
في جوليا هو 32 بت، وهو ليس هو نفس نوع الحرف العريض (wchar_t
أو wint_t
) على جميع المنصات.
نوع الإرجاع Union{}
يعني أن الدالة لن ترجع، أي ما يعادل [[noreturn]]
في C++11 أو _Noreturn
في C11 (مثل jl_throw
أو longjmp
). لا تستخدم هذا للوظائف التي لا ترجع قيمة (void
) ولكنها ترجع، بالنسبة لتلك، استخدم Cvoid
بدلاً من ذلك.
بالنسبة لوسائط wchar_t*
، يجب أن يكون نوع جوليا Cwstring
(إذا كانت الروتين C تتوقع سلسلة منتهية بـ null)، أو Ptr{Cwchar_t}
بخلاف ذلك. لاحظ أيضًا أن بيانات سلسلة UTF-8 في جوليا منتهية داخليًا بـ null، لذا يمكن تمريرها إلى دوال C التي تتوقع بيانات منتهية بـ null دون الحاجة إلى عمل نسخة (لكن استخدام نوع Cwstring
سيتسبب في حدوث خطأ إذا كانت السلسلة نفسها تحتوي على أحرف null).
يمكن استدعاء دوال 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
بالنسبة لدوال 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))
قد تضيف مترجمات Fortran قد أيضًا معلمات مخفية أخرى للمؤشرات، والمصفوفات ذات الشكل المفترض (:
) والمصفوفات ذات الحجم المفترض (*
). يمكن تجنب هذا السلوك باستخدام ISO_C_BINDING
وإدراج bind(c)
في تعريف الروتين الفرعي، وهو موصى به بشدة للشفرة القابلة للتشغيل المتداخل. في هذه الحالة، لن تكون هناك أي معلمات مخفية، على حساب بعض ميزات اللغة (على سبيل المثال، سيتم السماح فقط بـ character(len=1)
لتمرير السلاسل النصية).
سترجع دالة 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
- [for
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
الإغلاق @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
هي:
زوج
(:function, "library")
(الأكثر شيوعًا)،أو
اسم الرمز
:function
أو سلسلة الاسم"function"
(للرموز في العملية الحالية أو libc)،أو
مؤشر دالة (على سبيل المثال، من
dlsym
).نوع إرجاع الدالة
زوج من أنواع الإدخال، يتوافق مع توقيع الدالة. واحدة من الأخطاء الشائعة هي نسيان أن 1-زوج من أنواع الوسائط يجب أن يُكتب مع فاصلة متروكة.
القيم الفعلية للوسائط التي سيتم تمريرها إلى الدالة، إن وجدت؛ كل منها هو معلمة منفصلة.
يجب أن تكون مجموعة (:function, "library")
ونوع الإرجاع وأنواع الإدخال ثوابت حرفية (أي، لا يمكن أن تكون متغيرات، ولكن انظر إلى Non-constant Function Specifications).
تُقيَّم المعلمات المتبقية في وقت الترجمة، عندما يتم تعريف الطريقة المحتوية.
جدول الترجمات بين واجهات الماكرو والوظائف موضح أدناه.
@ccall | ccall |
---|---|
@ccall clock()::Int32 | ccall(:clock, Int32, ()) |
@ccall f(a::Cint)::Cint | ccall(:a, Cint, (Cint,), a) |
@ccall "mylib".f(a::Cint, b::Cdouble)::Cvoid | ccall((:f, "mylib"), Cvoid, (Cint, Cdouble), (a, b)) |
@ccall $fptr.f()::Cvoid | ccall(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)::Cint | ccall(: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
) نرى نفس gethostname
ccall
كما هو مذكور أعلاه، ولكن مع التوقيع الصحيح لـ 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!
.
قد لا يتم العثور على هذا الرمز 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.