استخدام دوال مكتبات السي داخل برنامج الأسس


في هذا الدرس سنتعلم كيفية استخدام مكتبة متحركة (ديناميكية) مكتوبة بلغة السي البرمجية، داخل برنامج الأسس، حيث يمكننا تحميل أي مكتبة متحركة مهما كانت اللغة التي كتبت بها وعند تحميلها تكون كل دالّاتها العمومية متوفرة للاستخدام من داخل برنامج الأسُس، لكن ستحتاج لتعريف تلك الدوال يدوياً باستخدام الأمر "دالة" مع إضافة المبدل `@تصدير` (expname@) بالشكل التالي:

@تصدير[اسم_دالة_السي] دالة اسم_الدالة (تعريفات_المعطيات): صنف_الإرجاع؛
@expname[c_function_name] function function_name (arg_defs): return_type;

اسم_دالة_السي (c_function_name): اسم الدالة كما هو معرف بلغة السي، أي كما هو مذكور داخل المكتبة الديناميكية.
اسم_الدالة (function_name): اسم الدالة ضمن لغة الأسس. هذا هو الاسم الذي تستخدمه لاستدعاء الدالة ولا يُشترط أن يطابق اسم دالة السي.
تعريفات_المعطيات (arg_defs): تعريفات معطيات الدالة. يجب أن تطابق هذه التعريفات تعريف الدالة بلغة السي من حيث الأصناف وتسلسلها، ولكن ليس بالضرورة من حيث الأسماء.
صنف_الإرجاع (return_type): صنف القيمة المُعادة.

مثال:
في المثال التالي سيكون لدينا مكتبة C متحركة قمت بإنشائها اسمها "libtest.so"، تحتوي الدالة التالية المكتوبة بلغة C:

#include <stdio.h>
int square(int i) {
  return i * i;
}

الآن سأقوم بتضمين هذه المكتبة وتعريف دوالها (هنا لدينا دالة وحيدة هي square) ضمن برنامج الأسس، وبعد ذلك سأقوم باستخدام محتويات المكتبة:

  اشمل "مـتم/طـرفية"؛
  اشمل "libtest.so"؛ // تضمين المكتبة
  @تصدير[square] دالة تربيع (ع: صحيح): صحيح؛ // تعريف الدالة
  عرف س: صحيح = 10؛
  مـتم.طـرفية.اطبع("%d"، تربيع(س))؛ // 100
import "Srl/Console.alusus";
import "libtest.so"; // تضمين المكتبة
@expname[square] function square (i: int):int; // تعريف الدالة
def x: int = 10;
Srl.Console.print("%d", square(x)); // 100

ملاحظات
- عندما يصادف مترجم الأسس عبارة `اشمل` (import) فإنه يقوم بالبحث عن المكتبة المطلوبة في المجلد الذي فيه الملف (الذي نكتب فيه الكود) الذي تتم ترجمته، فإن لم يجد يبحث في في مجلد Lib داخل مجلد الأسس، فإن لم يجد يبحث في ملفات النظام.

- لايشترط أن يكون اسم الدالة مطابقاً لاسم الدالة في مكتبة السي، أي في المثال السابق كان بإمكانك تسمية الدالة pow أو أي اسم آخر، كما هو ملاحظ في المثال العربي أعلاه، وكما في المثال التالي:

import "Srl/Console.alusus";
import "libtest.so"; // تضمين المكتبة
use Srl.Console;
@expname[square] function pow (i: int):int; // تعريف الدالة
def x: int = 10;
print("%d", pow(x)); // 100

استخدام دوال مكتبات السي التي تستخدم أصنافاً غير أساسية


في حال كانت دوال مكتبة السي تستخدم أصنافاً غير أساسية (أي أصناف struct)، سنحتاج لتعريف نفس الصنف في الأسس، أي تعريف صنف فيه نفس المتغيرات بنفس الاصناف ونفس التسلسل.

مثال:

إضافةً إلى الدالة `تربيع` لدينا الدالة التالية المعرّفة ضمن المكتبة السابقة، حيث تقوم هذه الدالة بإضافة قيم محددة إلى القسم الحقيقي والتخيلي لعدد مركب:

#include <stdio.h>
int square(int i) {
  return i * i;
}

typedef struct Complex {
  float real;
  float imag;
} complex;

void addNumbers(complex c, int a,int b, complex *result) {
 result->real = c.real + a;
 result->imag = c.imag + b;
}

كما تلاحظ فإن الدالة addNumbers تأخذ 4 معطيات، الأول هو العدد المركب، والثاني هو القيمة المراد إضافتها إلى القسم الحقيقي، والثالث هو القيمة المراد إضافتها للقسم التخيلي، أما المعطى الأخير فهو مرجع من البنية complex.
الآن لو أردنا استخدام هذه الدوال ضمن برنامج الأسس فيجب أن نقوم بتعريف الصنف Complex ضمن برنامج الأسس ثم تعريف الدوال كما تعلمنا في الفقرة السابقة:

اشمل "مـتم/طـرفية"؛
اشمل "libtest.so"؛
استخدم مـتم.طـرفية؛
// تعريف الصنف.
صنف عـدد_مركب {
  عرف حقيقي: عائم؛
  عرف تخيلي: عائم؛
}
// تعريف الدالة التي تستخدم هذا الصنف.
@تصدير[addNumbers]
دالة اجمع (م: عـدد_مركب، ا: صحيح, ب: صحيح, ناتج: مؤشر[عـدد_مركب])؛

عرف ع: عـدد_مركب؛ // تعريف كائن من هذا الصنف ليمثل عددا مركبا
عرف ناتج: عـدد_مركب؛ // كائن آخر لتخزين النتيجة
// إسناد قيم للعدد المركب
ع.حقيقي = 1.1؛
ع.تخيلي = -2.4؛
// استخدام الدالة السابقة
اجمع(ع , 30, 2.4, ناتج~مؤشر)؛
طـرفية.اطبع("\nناتج.حقيقي = %.1f\n", ناتج.حقيقي~مثل[عائم[64]])؛
طـرفية.اطبع("ناتج.تخيلي = %.1f", ناتج.تخيلي~مثل[عائم[64]])؛
/*
ناتج.حقيقي = 31.1؛
ناتج.تخيلي = 0.0؛
*/
import "Srl/Console.alusus";
import "Srl/String.alusus"
import "libtest.so";
use Srl;
// تعريف الصنف.
class Complex {
  def real: float;
  def imag: float;
}
// تعريف الدالة التي تستخدم هذا الصنف.
@expname[addNumbers]
function sum (c: Complex, a: int, b: int, res: ptr[Complex]);

def c: Complex; // تعريف كائن من هذا الصنف ليمثل عددا مركبا
def result: Complex; // كائن آخر لتخزين النتيجة
// إسناد قيم للعدد المركب
c.real = 1.1;
c.imag = -2.4;
// استخدام الدالة السابقة
sum(c,30,2.4,result~ptr);
Console.print("\nresult.real = %.1f\n", result.real~cast[Float[64]]);
Console.print("result.imag = %.1f", result.imag~cast[Float[64]]);
/*
result.real = 31.1
result.imag = 0.0
*/

ملاحظات:
- في المثال السابق كان بالإمكان استخدام سند من الصنف `عـدد_مركب` (Complex) بدلاً من مؤشر.
- المترجم في حالة أصناف المستتخدم (struct) لا يكترث لاسم الصنف ولا لأسماء الأعضاء، وإنما فقط لتسلسل المتغيرات وأصنافها، أي في المثال السابق كان بالإمكان استخدام أسماء مختلفة للمتغيرين real و imag المُعرفين ضمن الصنف Complex، كما هو ملاحظ من النسخة العربية.
- تعريف كل تفاصيل الصنف ليست ضرورية في حالة كنت في برنامج الأسس تتعامل فقط مع مؤشر لذلك الصنف ولا تتعامل مباشرة مع أعضاءه الداخلية (أي في حال لم تكن بحاجة إلى الوصول المباشر إلى عناصره).
لتفهم المقصود من ذلك أكثر تابع المثال التالي:
هنا لدينا المكتبة السابقة libtest.so تحتوي البنية Point، وتحتوي دالتين الأولى لإنشاء كائن من هذه البنية وإرجاع مؤشر عليه، والثاني يأخذ مؤشراً من هذه البنية ويقوم بطباعة معلوماته.

// C
#include < stdio.h>
struct Point {
  int x;
  int y;
};
// دالة تقوم بإنشاء كائن من البنية السابقة، وتقوم بإرجاع مؤشر عليه.
struct Point* createPoint(int x, int y) {
  static struct Point p;
  struct Point *ptr;
  p.x = x;
  p.y = y;
  ptr = &p;
  return ptr;
};
// دالة تستلم مؤشراً من البنية السابقة، وتطبع محتوياتها.
void printPoint(struct Point *p) {
  printf("\nDisplaying information\n");
  printf("\nx: %d", p->x);
  printf("\ny: %d", p->y);
};

الآن في الأسس يمكننا أن نكتب:

// الأسس
اشمل "مـتم/طـرفية"؛
اشمل "libtest.so"؛
استخدم مـتم.طـرفية؛

صـنف نـقطة {}
@تصدير[createPoint] دالة أنشئ_نقطة (س: صحيح, ص: صحيح): مؤشر[نـقطة]؛
@تصدير[printPoint] دالة اطبع_نقطة (م: مؤشر[نـقطة])؛

عرف ن: مؤشر[نـقطة]؛
ن = أنشئ_نقطة(5, 10)؛
اطبع_نقطة(ن)؛
/*
Displaying information
x: 5
y: 10
*/
// Alusus
import "Srl/Console.alusus";
import "libtest.so";
use Srl.Console;

class Point {}
@expname[createPoint] function createPoint (x: int, y: int): ptr[Point];
@expname[printPoint] function printPoint (p: ptr[Point]);

def p: ptr[Point];
p = createPoint(5, 10);
printPoint(p);
/*
Displaying information
x: 5
y: 10
*/

لاحظ كيف قمنا بتعريف الصنف `نـقطة` (Point) ضمن برنامج الأسس. لقد قمنا بتركه فارغاً لأننا لن نستخدم أيّاً من عناصر هذا الصنف ضمن البرنامج، حيث أن كل مانفعله هنا هو إرسال مؤشرات إلى دوال السي (إنشاء نقطة) واستلام مؤشرات منها (تُرجع مؤشراً للصنف نـقطة) لاستخدامها في دوال سي أخرى (دالة الطباعة).
إذاً في البرنامج السابق نحن لانحتاج لتعريف عناصر الصنف الدخلية لأننا لانحاول استخدامها ضمن برنامج الأسس.
الآن لو جربنا الوصول إلى المتغير س أو ص من برنامج الأسس، فستحصل على خطأ لأن المتغير غير معرف ضمن برنامج الأسس:

ن~محتوى.س = 15؛ // خطأ: الصنف نـقطة لا يحتوى على العنصر س
p~cnt.x = 15; // error: x does not exist in type Point
أيضاً لو حاولنا القيام بشيء مثل:
عرف ن: نـقطة؛
اطبع_نقطة(ن~مؤشر)؛
def p: Point;
printPnt(p~ptr);

فذلك لن ينجح، لأنه في هذه الحالة حجز المتغير تم في جانب الأسس وتم باستخدام صنف لا يحتوي على x و y وبالتالي الأسس حجزت ذاكرة أصغر من احتياج دالة السي التي تعتقد أن الصنف يحتوي على x و y.
لذلك باختصار إذا كان كل تعاملك مع صنف من أصناف السي هو تبادل مؤشرات عليه مع دالات السي (كما هو الحال بالنسبة لصنف Fs.File) فإنك لست مضطرا لتعريف تفاصيل الصنف الداخلية لأنك في الأسس لن تتعامل مع العناصر الداخلية للصنف وإنما فقط مع مؤشرات للصنف، مع ملاحظة أن حجز متغير من صنف معين يتطلب التعامل مع عناصر الصنف لأنك تحجز الذاكرة للمتغير حسب عدد العناصر الموجودة. بمعنى آخر، إذا كنت تُعرف متغيرا من صنف من أصناف السي (متغير من الصنف، وليس متغير من مؤشر على الصنف) فإنك بحاجة لتعريف تفاصيل الصنف كاملة كي تضمن حجز الذاكرة بشكل صحيح.

يمكننا كتابة ثلاث نقاط أساسية إذا كانت محققة فيمكنك تجاهل عناصر الصنف:

  1. برامج الأسس تتعامل فقط مع مؤشرات لذلك الصنف ولا تتعامل مع متغيرات قيمية من ذلك الصنف.
  2. برامج الأسس لا تتعامل مع أي من عناصر الصنف.
  3. برامج الأسس لا تنشئ الصنف ولا تتلفه، وإنما تعتمد دائما على دالات السي لإنشاء وإتلاف ذلك الصنف.

إذا لم تتحقق الشروط الثلاث أعلاه فسيحتاج المستخدم لتعريف الصنف في الأسس مع تعريف كل عناصره.