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.

Note

حاليًا، يتطلب الربط الديناميكي مع مكتبة libjulia المشتركة تمرير خيار RTLD_GLOBAL. في بايثون، يبدو هذا كالتالي:

>>> julia=CDLL('./libjulia.dylib',RTLD_GLOBAL)
>>> julia.jl_init.argtypes = []
>>> julia.jl_init()
250593296
Note

إذا كان برنامج جوليا يحتاج إلى الوصول إلى الرموز من البرنامج التنفيذي الرئيسي، فقد يكون من الضروري إضافة علامة الربط -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

هناك بعض الوظائف للتحكم في جامع القمامة. في حالات الاستخدام العادية، لا ينبغي أن تكون هذه ضرورية.

FunctionDescription
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 لجوليا من تلك الخيوط.