Embedding Julia
كما رأينا في Calling C and Fortran Code، تمتلك جوليا طريقة بسيطة وفعالة لاستدعاء الدوال المكتوبة بلغة C. ولكن هناك حالات حيث يكون العكس مطلوبًا: استدعاء دوال جوليا من كود C. يمكن استخدام ذلك لدمج كود جوليا في مشروع أكبر بلغة C/C++، دون الحاجة إلى إعادة كتابة كل شيء بلغة C/C++. تمتلك جوليا واجهة برمجة تطبيقات C لجعل ذلك ممكنًا. نظرًا لأن معظم لغات البرمجة لديها طريقة لاستدعاء دوال C، يمكن أيضًا استخدام واجهة برمجة تطبيقات C الخاصة بجوليا لبناء جسور لغوية أخرى (مثل استدعاء جوليا من بايثون، راست أو C#). على الرغم من أن راست و C++ يمكنهما استخدام واجهة تضمين C مباشرة، إلا أن كلاهما لديه حزم تساعد في ذلك، بالنسبة لـ C++ Jluna مفيدة.
High-Level Embedding
ملاحظة: تغطي هذه القسم تضمين كود جوليا في C على أنظمة التشغيل الشبيهة بـ Unix. للقيام بذلك على Windows، يرجى الاطلاع على القسم التالي، High-Level Embedding on Windows with Visual Studio.
نبدأ ببرنامج C بسيط يقوم بتهيئة جوليا وينفذ بعض كود جوليا:
#include <julia.h>
JULIA_DEFINE_FAST_TLS // only define this once, in an executable (not in a shared library) if you want fast code.
int main(int argc, char *argv[])
{
/* required: setup the Julia context */
jl_init();
/* run Julia commands */
jl_eval_string("print(sqrt(2.0))");
/* strongly recommended: notify Julia that the
program is about to terminate. this allows
Julia time to cleanup pending write requests
and run all finalizers
*/
jl_atexit_hook(0);
return 0;
}
لبناء هذا البرنامج، يجب عليك إضافة مسار رأس جوليا إلى مسار التضمين والربط ضد libjulia
. على سبيل المثال، عندما يتم تثبيت جوليا في $JULIA_DIR
، يمكن للمرء تجميع برنامج الاختبار أعلاه test.c
باستخدام gcc
كالتالي:
gcc -o test -fPIC -I$JULIA_DIR/include/julia -L$JULIA_DIR/lib -Wl,-rpath,$JULIA_DIR/lib test.c -ljulia
بدلاً من ذلك، انظر إلى برنامج embedding.c
في شجرة مصدر جوليا في مجلد test/embedding/
. برنامج الملف cli/loader_exe.c
هو مثال بسيط آخر عن كيفية تعيين خيارات jl_options
أثناء الربط مع libjulia
.
الشيء الأول الذي يجب القيام به قبل استدعاء أي دالة C في جوليا هو تهيئة جوليا. يتم ذلك عن طريق استدعاء jl_init
، الذي يحاول تحديد موقع تثبيت جوليا تلقائيًا. إذا كنت بحاجة إلى تحديد موقع مخصص، أو تحديد أي صورة نظام لتحميلها، استخدم jl_init_with_image
بدلاً من ذلك.
العبارة الثانية في برنامج الاختبار تقيم عبارة جوليا باستخدام استدعاء إلى jl_eval_string
.
قبل أن ينتهي البرنامج، يُوصى بشدة باستدعاء jl_atexit_hook
. البرنامج المثال أعلاه يستدعي هذا قبل العودة من main
.
حاليًا، يتطلب الربط الديناميكي مع مكتبة libjulia
المشتركة تمرير خيار RTLD_GLOBAL
. في بايثون، يبدو هذا كالتالي:
>>> julia=CDLL('./libjulia.dylib',RTLD_GLOBAL)
>>> julia.jl_init.argtypes = []
>>> julia.jl_init()
250593296
إذا كان برنامج جوليا يحتاج إلى الوصول إلى الرموز من البرنامج التنفيذي الرئيسي، فقد يكون من الضروري إضافة علامة الربط -Wl,--export-dynamic
في وقت الترجمة على نظام لينكس بالإضافة إلى العلامات التي تم إنشاؤها بواسطة julia-config.jl
الموضحة أدناه. هذا ليس ضروريًا عند تجميع مكتبة مشتركة.
Using julia-config to automatically determine build parameters
تم إنشاء البرنامج النصي julia-config.jl
للمساعدة في تحديد معلمات البناء المطلوبة من قبل برنامج يستخدم جوليا المدمجة. يستخدم هذا البرنامج النصي معلمات البناء وتكوين النظام لتوزيعة جوليا المحددة التي يتم استدعاؤه بها لتصدير العلامات اللازمة للمجمع ليتفاعل برنامج التضمين مع تلك التوزيعة. يقع هذا البرنامج النصي في دليل البيانات المشتركة لجوليا.
Example
#include <julia.h>
int main(int argc, char *argv[])
{
jl_init();
(void)jl_eval_string("println(sqrt(2.0))");
jl_atexit_hook(0);
return 0;
}
On the command line
استخدام بسيط لهذا السكربت هو من سطر الأوامر. بافتراض أن julia-config.jl
موجود في /usr/local/julia/share/julia
، يمكن استدعاؤه من سطر الأوامر مباشرة ويأخذ أي مجموعة من ثلاثة أعلام:
/usr/local/julia/share/julia/julia-config.jl
Usage: julia-config [--cflags|--ldflags|--ldlibs]
إذا تم حفظ مصدر المثال أعلاه في الملف embed_example.c
، فإن الأمر التالي سيقوم بترجمته إلى برنامج تنفيذي على نظامي Linux وWindows (بيئة MSYS2). على macOS، استبدل gcc
بـ clang
.
/usr/local/julia/share/julia/julia-config.jl --cflags --ldflags --ldlibs | xargs gcc embed_example.c
Use in Makefiles
بشكل عام، ستكون مشاريع التضمين أكثر تعقيدًا من المثال أعلاه، لذا فإن ما يلي يسمح بدعم makefile العام أيضًا - بافتراض استخدام GNU make بسبب استخدام توسعات الماكرو shell. علاوة على ذلك، على الرغم من أن julia-config.jl
عادةً ما يكون في دليل /usr/local
، إذا لم يكن كذلك، فيمكن استخدام Julia نفسها للعثور على julia-config.jl
، ويمكن أن يستفيد makefile من ذلك. يتم توسيع المثال أعلاه لاستخدام makefile:
JL_SHARE = $(shell julia -e 'print(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia"))')
CFLAGS += $(shell $(JL_SHARE)/julia-config.jl --cflags)
CXXFLAGS += $(shell $(JL_SHARE)/julia-config.jl --cflags)
LDFLAGS += $(shell $(JL_SHARE)/julia-config.jl --ldflags)
LDLIBS += $(shell $(JL_SHARE)/julia-config.jl --ldlibs)
all: embed_example
الآن أمر البناء هو ببساطة make
.
High-Level Embedding on Windows with Visual Studio
إذا لم يتم إعداد متغير البيئة JULIA_DIR
، قم بإضافته باستخدام لوحة النظام قبل بدء Visual Studio. يجب أن يكون مجلد bin
تحت JULIA_DIR
موجودًا في مسار النظام.
نبدأ بفتح Visual Studio وإنشاء مشروع تطبيق وحدة تحكم جديد. افتح ملف الرأس 'stdafx.h'، وأضف الأسطر التالية في النهاية:
#include <julia.h>
ثم، استبدل دالة main() في المشروع بهذا الرمز:
int main(int argc, char *argv[])
{
/* required: setup the Julia context */
jl_init();
/* run Julia commands */
jl_eval_string("print(sqrt(2.0))");
/* strongly recommended: notify Julia that the
program is about to terminate. this allows
Julia time to cleanup pending write requests
and run all finalizers
*/
jl_atexit_hook(0);
return 0;
}
الخطوة التالية هي إعداد المشروع للعثور على ملفات تضمين جوليا والمكتبات. من المهم معرفة ما إذا كانت تثبيت جوليا 32 بت أو 64 بت. قم بإزالة أي تكوينات للمنصة لا تتوافق مع تثبيت جوليا قبل المتابعة.
باستخدام مربع حوار خصائص المشروع، انتقل إلى C/C++
| عام
وأضف $(JULIA_DIR)\include\julia\
إلى خاصية الدلائل الإضافية للتضمين. ثم، انتقل إلى قسم الرابط
| عام
وأضف $(JULIA_DIR)\lib
إلى خاصية دلائل المكتبة الإضافية. أخيرًا، تحت الرابط
| الإدخال
، أضف libjulia.dll.a;libopenlibm.dll.a;
إلى قائمة المكتبات.
في هذه المرحلة، يجب أن يتم بناء المشروع وتشغيله.
Converting Types
التطبيقات الحقيقية لن تحتاج فقط إلى تنفيذ التعبيرات، ولكن أيضًا إلى إرجاع قيمها إلى البرنامج المضيف. jl_eval_string
ترجع jl_value_t*
، وهو مؤشر إلى كائن جوليا مُخصص في الذاكرة. تخزين أنواع البيانات البسيطة مثل Float64
بهذه الطريقة يُسمى boxing
، واستخراج البيانات الأولية المخزنة يُسمى unboxing
. برنامجنا النموذجي المحسن الذي يحسب الجذر التربيعي لـ 2 في جوليا ويقرأ النتيجة مرة أخرى في C يحتوي الآن على هذا الكود:
jl_value_t *ret = jl_eval_string("sqrt(2.0)");
if (jl_typeis(ret, jl_float64_type)) {
double ret_unboxed = jl_unbox_float64(ret);
printf("sqrt(2.0) in C: %e \n", ret_unboxed);
}
else {
printf("ERROR: unexpected return type from sqrt(::Float64)\n");
}
للتحقق مما إذا كانت ret
من نوع جوليا محدد، يمكننا استخدام الدوال jl_isa
، jl_typeis
، أو jl_is_...
. من خلال كتابة typeof(sqrt(2.0))
في واجهة جوليا، يمكننا أن نرى أن نوع الإرجاع هو Float64
(double
في C). لتحويل القيمة المغلفة لجوليا إلى قيمة C double، يتم استخدام دالة jl_unbox_float64
في مقتطف الشيفرة أعلاه.
تُستخدم دوال jl_box_...
المقابلة للتحويل في الاتجاه الآخر:
jl_value_t *a = jl_box_float64(3.0);
jl_value_t *b = jl_box_float32(3.0f);
jl_value_t *c = jl_box_int32(3);
كما سنرى لاحقًا، يتطلب استخدام تقنية "البوكسينغ" لاستدعاء دوال جوليا مع معطيات محددة.
Calling Julia Functions
بينما يسمح jl_eval_string
للغة C بالحصول على نتيجة تعبير جوليا، إلا أنه لا يسمح بتمرير المعاملات المحسوبة في C إلى جوليا. للقيام بذلك، ستحتاج إلى استدعاء دوال جوليا مباشرة، باستخدام jl_call
:
jl_function_t *func = jl_get_function(jl_base_module, "sqrt");
jl_value_t *argument = jl_box_float64(2.0);
jl_value_t *ret = jl_call1(func, argument);
في الخطوة الأولى، يتم استرداد مقبض لدالة جوليا sqrt
عن طريق استدعاء jl_get_function
. الحجة الأولى المرسلة إلى jl_get_function
هي مؤشر إلى وحدة Base
التي تم تعريف sqrt
فيها. ثم، يتم تغليف القيمة العشرية باستخدام jl_box_float64
. أخيرًا، في الخطوة الأخيرة، يتم استدعاء الدالة باستخدام jl_call1
. توجد أيضًا دوال jl_call0
و jl_call2
و jl_call3
، لتسهيل التعامل مع أعداد مختلفة من الحجج. لتمرير المزيد من الحجج، استخدم jl_call
:
jl_value_t *jl_call(jl_function_t *f, jl_value_t **args, int32_t nargs)
حجته الثانية args
هي مصفوفة من jl_value_t*
الحجج و nargs
هو عدد الحجج.
هناك أيضًا طريقة بديلة، ربما أبسط، لاستدعاء دوال جوليا، وهي عبر @cfunction
. يسمح لك استخدام @cfunction
بإجراء تحويلات الأنواع على جانب جوليا، وهو عادةً أسهل من القيام بذلك على الجانب C. سيكون مثال sqrt
أعلاه مع @cfunction
مكتوبًا كالتالي:
double (*sqrt_jl)(double) = jl_unbox_voidpointer(jl_eval_string("@cfunction(sqrt, Float64, (Float64,))"));
double ret = sqrt_jl(2.0);
حيث نقوم أولاً بتعريف دالة قابلة للاستدعاء من C في جوليا، ثم نستخرج مؤشر الدالة منها، وأخيرًا نستدعيها. بالإضافة إلى تبسيط تحويلات الأنواع من خلال القيام بها في اللغة ذات المستوى الأعلى، فإن استدعاء دوال جوليا عبر مؤشرات @cfunction
يلغي عبء التوزيع الديناميكي المطلوب بواسطة jl_call
(الذي يتم فيه "تغليف" جميع المعاملات)، ويجب أن يكون له أداء يعادل مؤشرات الدوال الأصلية في C.
Memory Management
كما رأينا، يتم تمثيل كائنات جوليا في C كمؤشرات من نوع jl_value_t*
. يثير هذا السؤال حول من هو المسؤول عن تحرير هذه الكائنات.
عادةً ما يتم تحرير كائنات جوليا بواسطة جامع القمامة (GC)، لكن جامع القمامة لا يعرف تلقائيًا أننا نحتفظ بمرجع لقيمة جوليا من C. وهذا يعني أن جامع القمامة يمكنه تحرير الكائنات من تحتك، مما يجعل المؤشرات غير صالحة.
ستعمل GC فقط عندما يتم تخصيص كائنات جوليا جديدة. تستدعي مثل jl_box_float64
التخصيص، ولكن قد يحدث التخصيص أيضًا في أي نقطة أثناء تشغيل كود جوليا.
عند كتابة كود يتضمن جوليا، من الآمن عمومًا استخدام قيم jl_value_t*
بين استدعاءات jl_...
(حيث سيتم تشغيل جامع القمامة فقط بواسطة تلك الاستدعاءات). ولكن من أجل التأكد من أن القيم يمكن أن تبقى صامدة خلال استدعاءات jl_...
، يجب أن نخبر جوليا أننا لا زلنا نحتفظ بمرجع لقيم جوليا root، وهي عملية تُسمى "تثبيت القمامة". سيساعد تثبيت قيمة ما على ضمان عدم تحديد جامع القمامة لهذه القيمة على أنها غير مستخدمة عن طريق الخطأ وتحرير الذاكرة التي تدعم تلك القيمة. يمكن القيام بذلك باستخدام ماكرو JL_GC_PUSH
:
jl_value_t *ret = jl_eval_string("sqrt(2.0)");
JL_GC_PUSH1(&ret);
// Do something with ret
JL_GC_POP();
تطلق استدعاء JL_GC_POP
المراجع التي تم إنشاؤها بواسطة JL_GC_PUSH
السابقة. لاحظ أن JL_GC_PUSH
يخزن المراجع على مكدس C، لذا يجب أن يكون متطابقًا تمامًا مع JL_GC_POP
قبل الخروج من النطاق. أي، قبل أن تعود الدالة، أو يتدفق التحكم بطريقة أخرى خارج الكتلة التي تم فيها استدعاء JL_GC_PUSH
.
يمكن دفع عدة قيم من جوليا دفعة واحدة باستخدام ماكرو JL_GC_PUSH2
إلى JL_GC_PUSH6
:
JL_GC_PUSH2(&ret1, &ret2);
// ...
JL_GC_PUSH6(&ret1, &ret2, &ret3, &ret4, &ret5, &ret6);
لدفع مصفوفة من قيم جوليا، يمكن استخدام ماكرو JL_GC_PUSHARGS
، والذي يمكن استخدامه كما يلي:
jl_value_t **args;
JL_GC_PUSHARGS(args, 2); // args can now hold 2 `jl_value_t*` objects
args[0] = some_value;
args[1] = some_other_value;
// Do something with args (e.g. call jl_... functions)
JL_GC_POP();
يجب أن يحتوي كل نطاق على استدعاء واحد فقط لـ JL_GC_PUSH*
، ويجب أن يكون مقترنًا باستدعاء واحد فقط لـ JL_GC_POP
. إذا لم يكن بالإمكان دفع جميع المتغيرات الضرورية التي تريد تثبيتها بواسطة استدعاء واحد فقط لـ JL_GC_PUSH*
، أو إذا كان هناك أكثر من 6 متغيرات يجب دفعها ولم يكن استخدام مصفوفة من الوسائط خيارًا، فيمكن استخدام الكتل الداخلية:
jl_value_t *ret1 = jl_eval_string("sqrt(2.0)");
JL_GC_PUSH1(&ret1);
jl_value_t *ret2 = 0;
{
jl_function_t *func = jl_get_function(jl_base_module, "exp");
ret2 = jl_call1(func, ret1);
JL_GC_PUSH1(&ret2);
// Do something with ret2.
JL_GC_POP(); // This pops ret2.
}
JL_GC_POP(); // This pops ret1.
لاحظ أنه ليس من الضروري أن تكون لديك قيم صالحة من نوع jl_value_t*
قبل استدعاء JL_GC_PUSH*
. من المقبول أن يكون لديك عدد منها مهيأة إلى NULL
، وتمريرها إلى JL_GC_PUSH*
ثم إنشاء القيم الفعلية لجوليا. على سبيل المثال:
jl_value_t *ret1 = NULL, *ret2 = NULL;
JL_GC_PUSH2(&ret1, &ret2);
ret1 = jl_eval_string("sqrt(2.0)");
ret2 = jl_eval_string("sqrt(3.0)");
// Use ret1 and ret2
JL_GC_POP();
إذا كان من الضروري الاحتفاظ بمؤشر إلى متغير بين الدوال (أو نطاقات الكتل)، فلا يمكن استخدام JL_GC_PUSH*
. في هذه الحالة، من الضروري إنشاء والاحتفاظ بمرجع إلى المتغير في نطاق جولي العالمي. إحدى الطرق البسيطة لتحقيق ذلك هي استخدام IdDict
عالمي سيحتفظ بالمراجع، مما يتجنب إلغاء التخصيص بواسطة جامع القمامة. ومع ذلك، ستعمل هذه الطريقة بشكل صحيح فقط مع الأنواع القابلة للتغيير.
// This functions shall be executed only once, during the initialization.
jl_value_t* refs = jl_eval_string("refs = IdDict()");
jl_function_t* setindex = jl_get_function(jl_base_module, "setindex!");
...
// `var` is the variable we want to protect between function calls.
jl_value_t* var = 0;
...
// `var` is a `Vector{Float64}`, which is mutable.
var = jl_eval_string("[sqrt(2.0); sqrt(4.0); sqrt(6.0)]");
// To protect `var`, add its reference to `refs`.
jl_call3(setindex, refs, var, var);
إذا كانت المتغير غير قابلة للتغيير، فإنه يجب أن يتم لفها في حاوية قابلة للتغيير مكافئة أو، من الأفضل، في RefValue{Any}
قبل دفعها إلى IdDict
. في هذا النهج، يجب إنشاء الحاوية أو ملؤها عبر كود C باستخدام، على سبيل المثال، الدالة jl_new_struct
. إذا تم إنشاء الحاوية بواسطة jl_call*
، فستحتاج إلى إعادة تحميل المؤشر لاستخدامه في كود C.
// This functions shall be executed only once, during the initialization.
jl_value_t* refs = jl_eval_string("refs = IdDict()");
jl_function_t* setindex = jl_get_function(jl_base_module, "setindex!");
jl_datatype_t* reft = (jl_datatype_t*)jl_eval_string("Base.RefValue{Any}");
...
// `var` is the variable we want to protect between function calls.
jl_value_t* var = 0;
...
// `var` is a `Float64`, which is immutable.
var = jl_eval_string("sqrt(2.0)");
// Protect `var` until we add its reference to `refs`.
JL_GC_PUSH1(&var);
// Wrap `var` in `RefValue{Any}` and push to `refs` to protect it.
jl_value_t* rvar = jl_new_struct(reft, var);
JL_GC_POP();
jl_call3(setindex, refs, rvar, rvar);
يمكن السماح لجمع القمامة (GC) بإلغاء تخصيص متغير عن طريق إزالة الإشارة إليه من refs
باستخدام الدالة delete!
، بشرط ألا يتم الاحتفاظ بأي إشارة أخرى إلى المتغير في أي مكان:
jl_function_t* delete = jl_get_function(jl_base_module, "delete!");
jl_call2(delete, refs, rvar);
كبديل للحالات البسيطة جدًا، من الممكن ببساطة إنشاء حاوية عالمية من النوع Vector{Any}
واسترجاع العناصر منها عند الحاجة، أو حتى إنشاء متغير عالمي واحد لكل مؤشر باستخدام
jl_module_t *mod = jl_main_module;
jl_sym_t *var = jl_symbol("var");
jl_binding_t *bp = jl_get_binding_wr(mod, var, 1);
jl_checked_assignment(bp, mod, var, val);
Updating fields of GC-managed objects
يعمل جامع القمامة أيضًا على افتراض أنه على دراية بكل كائن من الجيل القديم الذي يشير إلى كائن من الجيل الأصغر. في أي وقت يتم فيه تحديث مؤشر يكسر هذا الافتراض، يجب الإشارة إلى الجامع باستخدام دالة jl_gc_wb
(حاجز الكتابة) كما يلي:
jl_value_t *parent = some_old_value, *child = some_young_value;
((some_specific_type*)parent)->field = child;
jl_gc_wb(parent, child);
من المستحيل عمومًا التنبؤ بالقيم التي ستكون قديمة في وقت التشغيل، لذا يجب إدراج حاجز الكتابة بعد جميع التخزينات الصريحة. استثناء ملحوظ هو إذا كان كائن parent
قد تم تخصيصه للتو ولم يتم تشغيل جمع القمامة منذ ذلك الحين. لاحظ أن معظم دوال jl_...
يمكن أن تستدعي أحيانًا جمع القمامة.
حاجز الكتابة ضروري أيضًا لمصفوفات المؤشرات عند تحديث بياناتها مباشرة. يُفضل عادةً استدعاء jl_array_ptr_set
. ولكن يمكن إجراء التحديثات المباشرة. على سبيل المثال:
jl_array_t *some_array = ...; // e.g. a Vector{Any}
void **data = jl_array_data(some_array, void*);
jl_value_t *some_value = ...;
data[0] = some_value;
jl_gc_wb(jl_array_owner(some_array), some_value);
Controlling the Garbage Collector
هناك بعض الوظائف للتحكم في جامع القمامة. في حالات الاستخدام العادية، لا ينبغي أن تكون هذه ضرورية.
Function | Description |
---|---|
jl_gc_collect() | Force a GC run |
jl_gc_enable(0) | Disable the GC, return previous state as int |
jl_gc_enable(1) | Enable the GC, return previous state as int |
jl_gc_is_enabled() | Return current state as int |
Working with Arrays
يمكن لجوليا و C مشاركة بيانات المصفوفات دون نسخ. ستظهر المثال التالي كيف يعمل ذلك.
تمثل مصفوفات جوليا في C بواسطة نوع البيانات jl_array_t*
. بشكل أساسي، jl_array_t
هو هيكل يحتوي على:
- معلومات عن نوع البيانات
- مؤشر إلى كتلة البيانات
- معلومات عن أحجام المصفوفة
للحفاظ على الأمور بسيطة، نبدأ بمصفوفة أحادية البعد. يمكن إنشاء مصفوفة تحتوي على عناصر Float64 بطول 10 على النحو التالي:
jl_value_t* array_type = jl_apply_array_type((jl_value_t*)jl_float64_type, 1);
jl_array_t* x = jl_alloc_array_1d(array_type, 10);
بدلاً من ذلك، إذا كنت قد خصصت المصفوفة بالفعل، يمكنك إنشاء غلاف رقيق حول بياناتها:
double *existingArray = (double*)malloc(sizeof(double)*10);
jl_array_t *x = jl_ptr_to_array_1d(array_type, existingArray, 10, 0);
الحجة الأخيرة هي قيمة منطقية تشير إلى ما إذا كان يجب على جوليا أن تتولى ملكية البيانات. إذا كانت هذه الحجة غير صفرية، فسيقوم جامع القمامة GC
باستدعاء free
على مؤشر البيانات عندما لم يعد يتم الإشارة إلى المصفوفة.
للوصول إلى بيانات x
، يمكننا استخدام jl_array_data
:
double *xData = jl_array_data(x, double);
الآن يمكننا ملء المصفوفة:
for (size_t i = 0; i < jl_array_nrows(x); i++)
xData[i] = i;
الآن دعنا نستدعي دالة جوليا التي تقوم بعملية في المكان على x
:
jl_function_t *func = jl_get_function(jl_base_module, "reverse!");
jl_call1(func, (jl_value_t*)x);
من خلال طباعة المصفوفة، يمكن التحقق من أن عناصر x
قد تم عكسها الآن.
Accessing Returned Arrays
إذا كانت دالة جوليا ترجع مصفوفة، يمكن تحويل قيمة الإرجاع من jl_eval_string
و jl_call
إلى jl_array_t*
:
jl_function_t *func = jl_get_function(jl_base_module, "reverse");
jl_array_t *y = (jl_array_t*)jl_call1(func, (jl_value_t*)x);
الآن يمكن الوصول إلى محتوى y
كما كان من قبل باستخدام jl_array_data
. كما هو الحال دائمًا، تأكد من الاحتفاظ بمرجع إلى المصفوفة أثناء استخدامها.
Multidimensional Arrays
تُخزَّن مصفوفات جوليا متعددة الأبعاد في الذاكرة بترتيب رئيسي عمودي. إليك بعض الشيفرة التي تنشئ مصفوفة ثنائية الأبعاد وتصل إلى خصائصها:
// Create 2D array of float64 type
jl_value_t *array_type = jl_apply_array_type((jl_value_t*)jl_float64_type, 2);
int dims[] = {10,5};
jl_array_t *x = jl_alloc_array_nd(array_type, dims, 2);
// Get array pointer
double *p = jl_array_data(x, double);
// Get number of dimensions
int ndims = jl_array_ndims(x);
// Get the size of the i-th dim
size_t size0 = jl_array_dim(x,0);
size_t size1 = jl_array_dim(x,1);
// Fill array with data
for(size_t i=0; i<size1; i++)
for(size_t j=0; j<size0; j++)
p[j + size0*i] = i + j;
لاحظ أنه بينما تستخدم مصفوفات جوليا الفهرسة القائمة على 1، فإن واجهة برمجة التطبيقات C تستخدم الفهرسة القائمة على 0 (على سبيل المثال عند استدعاء jl_array_dim
) من أجل القراءة ككود C عادي.
Exceptions
يمكن أن ترمي كود جوليا استثناءات. على سبيل المثال، اعتبر:
jl_eval_string("this_function_does_not_exist()");
ستبدو هذه المكالمة وكأنها لا تفعل شيئًا. ومع ذلك، من الممكن التحقق مما إذا تم طرح استثناء:
if (jl_exception_occurred())
printf("%s \n", jl_typeof_str(jl_exception_occurred()));
إذا كنت تستخدم واجهة برمجة التطبيقات C الخاصة بـ Julia من لغة تدعم الاستثناءات (مثل Python أو C# أو C++)، فمن المنطقي لف كل استدعاء إلى libjulia
في دالة تتحقق مما إذا تم رمي استثناء، ثم تعيد رمي الاستثناء في لغة المضيف.
Throwing Julia Exceptions
عند كتابة دوال قابلة للاستدعاء في جوليا، قد يكون من الضروري التحقق من صحة المعاملات وإلقاء استثناءات للإشارة إلى الأخطاء. يبدو فحص النوع النموذجي كما يلي:
if (!jl_typeis(val, jl_float64_type)) {
jl_type_error(function_name, (jl_value_t*)jl_float64_type, val);
}
يمكن رفع الاستثناءات العامة باستخدام الدوال:
void jl_error(const char *str);
void jl_errorf(const char *fmt, ...);
jl_error
يأخذ سلسلة C، و jl_errorf
يتم استدعاؤه مثل printf
:
jl_errorf("argument x = %d is too large", x);
حيث في هذا المثال يُفترض أن x
هو عدد صحيح.
Thread-safety
بشكل عام، واجهة برمجة التطبيقات C الخاصة بـ Julia ليست آمنة تمامًا للخيوط. عند تضمين Julia في تطبيق متعدد الخيوط، يجب توخي الحذر لعدم انتهاك القيود التالية:
jl_init()
يمكن استدعاؤها مرة واحدة فقط في عمر التطبيق. وينطبق الشيء نفسه علىjl_atexit_hook()
، ويمكن استدعاؤها فقط بعدjl_init()
.- تُستخدم دوال واجهة برمجة التطبيقات
jl_...()
فقط من الخيط الذي تم فيه استدعاءjl_init()
، أو من الخيوط التي بدأها وقت تشغيل جوليا. إن استدعاء دوال واجهة برمجة التطبيقات لجوليا من خيوط بدأها المستخدم غير مدعوم، وقد يؤدي إلى سلوك غير محدد وعمليات تعطل.
الشرط الثاني أعلاه يعني أنه لا يمكنك استدعاء دوال jl_...()
بأمان من خيوط لم يتم بدءها بواسطة جوليا (كون الخيط الذي يستدعي jl_init()
هو الاستثناء). على سبيل المثال، ما يلي غير مدعوم ومن المرجح أن يؤدي إلى خطأ في الوصول إلى الذاكرة:
void *func(void*)
{
// Wrong, jl_eval_string() called from thread that was not started by Julia
jl_eval_string("println(Threads.threadid())");
return NULL;
}
int main()
{
pthread_t t;
jl_init();
// Start a new thread
pthread_create(&t, NULL, func, NULL);
pthread_join(t, NULL);
jl_atexit_hook(0);
}
بدلاً من ذلك، ستعمل جميع استدعاءات جوليا من نفس الخيط الذي أنشأه المستخدم:
void *func(void*)
{
// Okay, all jl_...() calls from the same thread,
// even though it is not the main application thread
jl_init();
jl_eval_string("println(Threads.threadid())");
jl_atexit_hook(0);
return NULL;
}
int main()
{
pthread_t t;
// Create a new thread, which runs func()
pthread_create(&t, NULL, func, NULL);
pthread_join(t, NULL);
}
مثال على استدعاء واجهة برمجة التطبيقات C الخاصة بـ Julia من خيط بدأته Julia نفسها:
#include <julia/julia.h>
JULIA_DEFINE_FAST_TLS
double c_func(int i)
{
printf("[C %08x] i = %d\n", pthread_self(), i);
// Call the Julia sqrt() function to compute the square root of i, and return it
jl_function_t *sqrt = jl_get_function(jl_base_module, "sqrt");
jl_value_t* arg = jl_box_int32(i);
double ret = jl_unbox_float64(jl_call1(sqrt, arg));
return ret;
}
int main()
{
jl_init();
// Define a Julia function func() that calls our c_func() defined in C above
jl_eval_string("func(i) = ccall(:c_func, Float64, (Int32,), i)");
// Call func() multiple times, using multiple threads to do so
jl_eval_string("println(Threads.threadpoolsize())");
jl_eval_string("use(i) = println(\"[J $(Threads.threadid())] i = $(i) -> $(func(i))\")");
jl_eval_string("Threads.@threads for i in 1:5 use(i) end");
jl_atexit_hook(0);
}
إذا قمنا بتشغيل هذا الكود باستخدام خيطين في جوليا، سنحصل على المخرجات التالية (ملاحظة: ستختلف المخرجات حسب التشغيل والنظام):
$ JULIA_NUM_THREADS=2 ./thread_example
2
[C 3bfd9c00] i = 1
[C 23938640] i = 4
[J 1] i = 1 -> 1.0
[C 3bfd9c00] i = 2
[J 1] i = 2 -> 1.4142135623730951
[C 3bfd9c00] i = 3
[J 2] i = 4 -> 2.0
[C 23938640] i = 5
[J 1] i = 3 -> 1.7320508075688772
[J 2] i = 5 -> 2.23606797749979
كما يتضح، فإن خيط جوليا 1 يتوافق مع معرف pthread 3bfd9c00، وخيط جوليا 2 يتوافق مع المعرف 23938640، مما يظهر أنه يتم استخدام عدة خيوط على مستوى C، وأنه يمكننا بأمان استدعاء روتينات واجهة برمجة التطبيقات C لجوليا من تلك الخيوط.