> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# البرمجة بعدة لغات Polyglot: متى تفيد ومتى تعقد مشروعك؟
- URL: https://arabroot.io/articles/البرمجة-بعدة-لغات-polyglot-متى-تفيد-ومتى-تعقد-مشروعك/
- Published: 2026-10-30T23:02:51.000Z
- Updated: 2026-10-31T06:54:01.000Z
- Description: نستدعي نفس الدالة من Python بخمس طرق، عبر FFI إلى C وGo وعبر HTTP وgRPC، ونقيس كلفة كل حد بين لغتين وتعقيد البناء وما يحدث عندما يتعطل الطرف الآخر.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

في مقال [كيف تختار لغة البرمجة لمشروعك؟](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%84%D8%BA%D8%A9-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%84%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/) تابعنا شركة ناشئة تبيع الشاي والقهوة عبر الإنترنت، واخترنا لها لغة يعرفها فريقها بدلاً من لغة يتحمس لها أحدهم. ومرت سنة، والخدمة الأساسية مكتوبة بلغة Python، ثم أضاف الفريق بحثاً يتحمل الأخطاء الإملائية، فإذا كتب العميل «كامومل» بدلاً من «كاموميل» بحرف ناقص، فإن البحث يقارن ما كتبه بأسماء المنتجات بدالة تحسب عدد التعديلات بين كلمتين، وهي ما يسمى مسافة التعديل Edit Distance أو مسافة Levenshtein. والمشكلة أن هذه الدالة بطيئة في Python عندما تكبر قائمة المنتجات، وفي اجتماع الفريق ظهرت ثلاثة اقتراحات: أن نكتب الدالة بلغة C ونستدعيها من Python مباشرة، أو أن نكتبها بلغة Go في خدمة مستقلة نكلمها عبر HTTP أو gRPC، أو أن نترك كل شيء في Python ونقبل البطء.

وهذا هو سؤال البرمجة بعدة لغات Polyglot Programming: متى يستحق النظام أن يضم أكثر من لغة، وكيف تربط بينها، وما الثمن؟ فالكثير من الأنظمة اليوم متعددة اللغات دون أن يقرر أحد ذلك صراحة، فالواجهة بلغة TypeScript والخادم بلغة Python أو Java، والسكربتات بلغة Bash، ومكتبة Python التي تستخدمها قد تكون مكتوبة في داخلها بلغة C أو Rust. وفي هذا المقال سوف نبني الطرق الثلاث فعلاً، ونقيس كم يكلف كل استدعاء، وكم تزيد خطوات البناء، وماذا يحدث عندما يتعطل الكود على الطرف الآخر.

وسوف نناقش في هذا المقال ما يلي:

- ما معنى Polyglot، وأشكاله الأربعة: الخدمات المنفصلة، والاستدعاء المباشر FFI، وبيئة التشغيل المشتركة، والواجهة والخادم.
- متى تفيد اللغة الثانية، ومتى تتحول إلى عبء على الفريق والبناء والتشغيل.
- المختبر: دالة واحدة بلغتي C وGo، نستدعيها من Python بخمس طرق ونقيس كلفة الاستدعاء والعمل الحقيقي.
- تعقيد البناء بالأرقام، والمشكلة التي ظهرت مع إصدار gRPC.
- ما الذي يحدث داخل استدعاء FFI واستدعاء الشبكة، ولماذا يختلف الثمن.
- العقد بين لغتين: الأرقام الكبيرة والمال والوقت والنصوص، ثم سؤال من يصون اللغة الثانية.
- ماذا يحدث إذا أخطأت في تعريف الدالة، أو وصل مدخل سيئ إلى C، أو توقفت الخدمة؟
- قصتان موثقتان: Discord مع Elixir وRust، ومكتبة cryptography في Python عندما أضافت Rust.
- جدول قرار وأخطاء شائعة وقائمة تحقق.

## ما هي البرمجة بعدة لغات Polyglot Programming؟

البرمجة بعدة لغات هي أن يتكون النظام الواحد من أجزاء مكتوبة بلغات مختلفة تعمل معاً. والكلمة تجمع تحتها أشكالاً مختلفة جداً في تكلفتها، لذلك قبل أن تقرر يجب أن تعرف أي شكل تقصد:

![أربعة أشكال للبرمجة بعدة لغات: خدمات منفصلة تتكلم عبر الشبكة بواجهات HTTP أو gRPC ولكل خدمة لغتها، واستدعاء مباشر داخل نفس العملية FFI مثل Python تستدعي C أو Rust، ولغات على بيئة تشغيل واحدة مثل Java وKotlin على JVM أو C# وF# على .NET، وواجهة بلغة وخادم بلغة أخرى مثل TypeScript في المتصفح وPython في الخادم](https://arabroot.io/content/images/2026/10/polyglot-programming-01-forms.webp)

نفس الكلمة لأربعة أشكال مختلفة، والفرق بينها في الحد الفاصل: شبكة، أو ذاكرة مشتركة، أو بيئة تشغيل واحدة

- الخدمات المنفصلة Services: كل خدمة بلغتها، وتتكلم الخدمات عبر الشبكة بواجهة HTTP مع JSON أو gRPC أو طابور رسائل. وهذا الشكل المعتاد في الخدمات المصغرة Microservices، والحد الفاصل بين اللغتين هو الشبكة.
- الاستدعاء المباشر Foreign Function Interface واختصاراً FFI: لغة تستدعي دالة مكتوبة بلغة أخرى داخل نفس العملية Process ونفس الذاكرة، مثل Python التي تستدعي مكتبة C بالوحدة [ctypes](https://docs.python.org/3/library/ctypes.html?ref=arabroot.io)، أو Go التي تستدعي C عبر [cgo](https://pkg.go.dev/cmd/cgo?ref=arabroot.io). والحد الفاصل هنا هو واجهة الدالة فقط، ولا شيء يحمي إحدى اللغتين من أخطاء الأخرى.
- بيئة تشغيل مشتركة Shared Runtime: لغات مختلفة تترجم إلى نفس بيئة التشغيل، مثل Java وKotlin وScala على الـ JVM، أو C# وF# على .NET، فتتشارك المكتبات والذاكرة وجامع القمامة Garbage Collector. وهذا أرخص الأشكال، لأن الأدوات والنشر واحدة، وقد شرحنا بيئات التشغيل في مقال [المترجم والمفسر وJIT](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%AA%D8%B1%D8%AC%D9%85-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%B3%D8%B1-%D9%88-jit-%D9%88-webassembly-%D8%A8%D8%A7%D9%84%D8%AA%D9%81%D8%B5%D9%8A%D9%84/).
- الواجهة والخادم Frontend وBackend: المتصفح يفرض JavaScript أو TypeScript، والخادم بلغة أخرى. وهذا أكثر الأشكال انتشاراً، وأغلب الفرق تقبله دون نقاش، والسؤال الذي يبقى فيه هو هل تكتب الخادم أيضاً بلغة TypeScript لتكون لغة واحدة.

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

## متى تفيد اللغة الثانية؟

هناك أسباب حقيقية تجعل اللغة الثانية قراراً صحيحاً، وأغلبها يرجع إلى أن لغة معينة تملك شيئاً لا تملكه لغتك الأساسية:

- المكتبات والمنظومة Ecosystem: إذا كان نظامك بلغة Go أو Java واحتجت تدريب نماذج تعلم آلة، فالمكتبات في Python، وكتابة خدمة Python صغيرة لهذا الجزء أرخص بكثير من إعادة اختراع المكتبات. وهذا أكثر أسباب Polyglot شيوعاً وأصدقها.
- الطريق الساخن Hot Path: دالة واحدة تستهلك أغلب وقت المعالج، وكتابتها بلغة أسرع تغير النتيجة كثيراً. وهذا ما فعلته Discord كما سوف نرى، وهو ما تفعله مكتبات Python المشهورة منذ سنوات، فمكتبة NumPy في داخلها C، ومكتبة [pydantic-core](https://github.com/pydantic/pydantic-core?ref=arabroot.io) في داخلها Rust.
- الأنظمة القديمة Legacy: نظام مكتوب بلغة قديمة يعمل منذ سنوات ولا يمكن إعادة كتابته، فتكتب الأجزاء الجديدة بلغة حديثة وتربطها به.
- استقلال الفرق: في شركة كبيرة فيها فرق كثيرة، قد يكون من المعقول أن يختار كل فريق لغة خدمته، بشرط أن تكون الواجهات بين الخدمات واضحة، وقد ناقشنا هذا في مقال [Monolith أم Microservices](https://arabroot.io/articles/monolith-%D8%A7%D9%85-microservices-%D9%82%D8%B1%D8%A7%D8%B1-%D8%B5%D8%A7%D8%AF%D9%82-%D9%84%D9%84%D9%81%D8%B1%D9%82-%D8%A7%D9%84%D8%B5%D8%BA%D9%8A%D8%B1%D8%A9-%D9%88%D8%A7%D9%84%D9%83%D8%A8%D9%8A%D8%B1%D8%A9/).
- المنصة: المتصفح والجوال والأجهزة المدمجة تفرض لغات معينة، فلا مفر من أكثر من لغة.

وهناك طريق ثالث بين الخدمة المنفصلة وFFI بدأ ينتشر في السنوات الأخيرة، وهو WebAssembly: تترجم الدالة من لغة مثل Rust أو C أو Go إلى وحدة WebAssembly، ثم تشغلها داخل برنامجك في بيئة معزولة Sandbox لا تستطيع أن تلمس ذاكرة البرنامج المضيف إلا عبر واجهة محددة. فتحصل على سرعة قريبة من الكود الأصلي مع حد أعطال أوضح من FFI، مقابل أدوات أحدث وأقل نضجاً، وقد شرحنا فكرته في مقال [المترجم والمفسر وJIT وWebAssembly](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%AA%D8%B1%D8%AC%D9%85-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%B3%D8%B1-%D9%88-jit-%D9%88-webassembly-%D8%A8%D8%A7%D9%84%D8%AA%D9%81%D8%B5%D9%8A%D9%84/).

## متى تعقد اللغة الثانية مشروعك؟

وفي الجهة الأخرى، كل لغة تضيفها تضيف معها قائمة من التكاليف التي لا تظهر في يوم القرار، وإنما بعد أشهر:

![تكاليف اللغة الثانية: سلسلة أدوات بناء إضافية في كل جهاز وكل خط CI، ومنظومة حزم ثانية تحتاج متابعة الثغرات والتحديثات، وتوظيف أصعب لأنك تحتاج من يعرف اللغتين، ومنطق مكرر قد يختلف بين اللغتين مثل حساب الضريبة، وتنقيح أخطاء عبر الحدود حيث لا يرى المنقح الطرفين، وحدود أعطال حيث خطأ في C يسقط عملية Python كلها](https://arabroot.io/content/images/2026/10/polyglot-programming-02-costs.webp)

كل لغة تضيفها تأتي معها سلسلة أدوات ومنظومة حزم وحدود جديدة للأعطال

- سلسلة أدوات Toolchain إضافية: مترجم ثان ومدير حزم ثان، يجب أن يكونا في جهاز كل مطور وفي كل خط CI وفي كل Image. وقد رأيت في المقال السابق أن إصدار بيئة التشغيل وحده يغير السلوك.
- منظومة حزم ثانية: ثغرات تتابعها في npm وفي PyPI وفي وحدات Go، وتحديثات تجربها في مكانين، وقد شرحنا مخاطر الاعتماديات في مقال [مديرو الحزم](https://arabroot.io/articles/%D9%85%D8%AF%D9%8A%D8%B1%D9%88-%D8%A7%D9%84%D8%AD%D8%B2%D9%85-npm-%D9%88-pip-%D9%88-maven-%D9%88-cargo/).
- التوظيف والمعرفة: الشخص الذي يصلح العطل في الثالثة فجراً يجب أن يفهم اللغتين، أو على الأقل يفهم الحدود بينهما.
- المنطق المكرر: إذا احتاجت اللغتان نفس القاعدة، كحساب الضريبة أو التحقق من رقم الهاتف، فسوف تكتبها مرتين، ورأينا في المقال السابق أن نفس المعادلة أعطت ضريبة مختلفة بهللة بين Python والبقية.
- التنقيح Debugging عبر الحدود: المنقح Debugger في Python لا يرى ما يحدث داخل دالة C، وتتبع طلب يمر بثلاث خدمات يحتاج أدوات مثل [التتبع الموزع Distributed Tracing](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9-distributed-tracing-%D9%85%D8%B9-opentelemetry/).
- حدود الأعطال: خطأ ذاكرة في دالة C داخل عملية Python يسقط العملية كلها، وسوف نرى ذلك في المختبر.

لذلك فالسؤال ليس «هل اللغة الثانية أفضل لهذا الجزء؟»، لأن الإجابة غالباً نعم، وإنما «هل الفائدة في هذا الجزء أكبر من كل هذه التكاليف لسنوات؟».

## المختبر Lab: دالة واحدة وخمس طرق لاستدعائها

لنعد إلى فريق الشاي. الدالة البطيئة هي مسافة التعديل بين كلمتين، وسوف نكتبها بثلاث لغات: Python كمرجع، وC كمكتبة مشتركة Shared Library، وGo مرة كمكتبة مشتركة ومرة كخدمة شبكة. ونضيف دالة ثانية تافهة `add(a, b)` تجمع رقمين، لأن الغرض منها أن نقيس كلفة الاستدعاء نفسه دون أي عمل. والصورة التالية تبين الطرق الخمس:

![مخطط المختبر: برنامج Python واحد يستدعي الدالة بخمس طرق: Python مباشرة، ومكتبة C باسم liblev.so عبر ctypes، ومكتبة Go باسم libgolev.so عبر ctypes وcgo داخل نفس العملية، وخدمة Go منفصلة عبر HTTP مع JSON على المنفذ 8080، ونفس الخدمة عبر gRPC على المنفذ 9090، وكل ذلك في Container واحد بأربعة معالجات](https://arabroot.io/content/images/2026/10/polyglot-programming-03-lab.webp)

ثلاث طرق داخل نفس العملية، وطريقان عبر الشبكة إلى خدمة Go على نفس الجهاز

وكل شيء يعمل في Container واحد من `debian:trixie-slim` فيه gcc وGo 1.24.4 وPython 3.13.5 مع grpcio 1.51.1 من حزم Debian، والخدمة على نفس الجهاز، لذلك فأرقام الشبكة هنا هي أفضل حالة ممكنة، وفي شبكة حقيقية بين سيرفرين سوف تكون أكبر.

### الطريقة الأولى: دالة C عبر ctypes

دالة C بسيطة تحسب المسافة بصفين فقط من جدول البرمجة الديناميكية بدلاً من الجدول كاملاً:

```c
#include <stdlib.h>
#include <string.h>

long add(long a, long b) { return a + b; }

int lev(const char *a, const char *b) {
    size_t n = strlen(a), m = strlen(b);
    int *prev = malloc((m + 1) * sizeof(int)), *cur = malloc((m + 1) * sizeof(int));
    for (size_t j = 0; j <= m; j++) prev[j] = (int)j;
    for (size_t i = 1; i <= n; i++) {
        cur[0] = (int)i;
        for (size_t j = 1; j <= m; j++) {
            int del = prev[j] + 1, ins = cur[j - 1] + 1, sub = prev[j - 1] + (a[i - 1] != b[j - 1]);
            cur[j] = del < ins ? (del < sub ? del : sub) : (ins < sub ? ins : sub);
        }
        int *t = prev; prev = cur; cur = t;
    }
    int r = prev[m];
    free(prev); free(cur);
    return r;
}
```

ونبنيها مكتبة مشتركة بالأمر `gcc -O2 -shared -fPIC -o /build/liblev.so c/lev.c`، ثم نحملها من Python ونعرف لكل دالة أنواع مدخلاتها ومخرجها:

```python
c = ctypes.CDLL("/build/liblev.so")
c.add.argtypes, c.add.restype = (ctypes.c_long, ctypes.c_long), ctypes.c_long
c.lev.argtypes, c.lev.restype = (ctypes.c_char_p, ctypes.c_char_p), ctypes.c_int
```

ولاحظ أن هذين السطرين ليسا تجميلاً، فالمكتبة المشتركة لا تحمل أي معلومة عن أنواع دوالها، وctypes لا تعرف أن `add` تأخذ `long` إلا إذا أخبرتها، وسوف نرى في تجربة «ماذا يحدث إذا» ما الذي يحدث عندما تنساهما.

### الطريقة الثانية: دالة Go كمكتبة C

Go تستطيع أن تبني مكتبة مشتركة تبدو من الخارج كمكتبة C، بوضع البناء [\-buildmode=c-shared](https://pkg.go.dev/cmd/go?ref=arabroot.io#hdr-Build%5Fmodes) وتعليق `//export` فوق كل دالة. ووضعنا منطق المسافة في حزمة مستقلة `levcore` حتى تستخدمها المكتبة والخدمة معاً دون تكرار:

```go
// Built with: go build -buildmode=c-shared -o libgolev.so ./cmd/golev   (Python loads it with ctypes)
package main

import "C"
import "lab/levcore"

//export GoAdd
func GoAdd(a, b C.long) C.long { return a + b }

//export GoLev
func GoLev(a, b *C.char) C.int { return C.int(levcore.Levenshtein(C.GoString(a), C.GoString(b))) }

func main() {}
```

في الكود أعلاه لاحظ التالي:

- `import "C"` يفعل cgo، وهو الجسر بين Go وC.
- `C.GoString(a)` ينسخ النص من ذاكرة C إلى نص Go، لأن Go لا تسمح لجامع القمامة الخاص بها أن يدير ذاكرة لا يملكها. وهذا النسخ له ثمن سوف يظهر في الأرقام.
- الدالة `main` فارغة ولكنها مطلوبة، لأن المكتبة تحمل معها بيئة تشغيل Go كاملة، بجامع القمامة والجدولة، داخل عملية Python.

### الطريقة الثالثة والرابعة: خدمة Go عبر HTTP ثم gRPC

والخدمة تستخدم نفس الحزمة `levcore`، وتعرض الدالتين بطريقتين: HTTP مع JSON على المنفذ 8080، وgRPC على المنفذ 9090\. وواجهة [gRPC](https://grpc.io/docs/what-is-grpc/introduction/?ref=arabroot.io) تبدأ بملف يصف الرسائل والدوال بلغة [Protocol Buffers](https://protobuf.dev/?ref=arabroot.io):

```protobuf
syntax = "proto3";
package lev;
option go_package = "lab/levpb";

message AddRequest { int64 a = 1; int64 b = 2; }
message AddReply   { int64 sum = 1; }
message Pair       { string a = 1; string b = 2; }
message PairBatch  { repeated Pair pairs = 1; }
message Distances  { repeated int32 d = 1; }

service Lev {
  rpc Add (AddRequest) returns (AddReply);
  rpc Distance (PairBatch) returns (Distances);
}
```

ومن هذا الملف تولد أداة `protoc` كود Go للخادم، وتولد أداة `grpc_tools` كود Python للعميل، فيصبح الملف هو العقد Contract بين اللغتين، وأي تغيير في الرسالة يظهر خطأ ترجمة بدلاً من خطأ وقت التشغيل. ولاحظ أن الدالة `Distance` تأخذ دفعة من الأزواج Batch وليس زوجاً واحداً، وسوف نرى لماذا هذا القرار مهم. والكود الكامل للخادم وبرنامج القياس `bench.py` في مجلد `lab` مع المقال، وأهم ما في جانب Python هو الاتصال المفتوح الذي نعيد استخدامه:

```python
conn = http.client.HTTPConnection("127.0.0.1", 8080)          # one keep-alive connection
def http_add(a, b):
    conn.request("GET", f"/add?a={a}&b={b}")
    return int(conn.getresponse().read())

stub = lev_pb2_grpc.LevStub(grpc.insecure_channel("127.0.0.1:9090"))
def grpc_add(a, b):
    return stub.Add(lev_pb2.AddRequest(a=a, b=b)).sum
```

## تعقيد البناء Build Complexity بالأرقام

قبل أن نقيس السرعة لنقس ما يدفعه الفريق كل يوم: خطوات البناء. كتبنا السكربت `build.sh` الذي يبني كل الأجزاء ويقيس وقت كل خطوة، وفي أول تشغيل توقف عند هذه الرسالة:

```text
go: downloading google.golang.org/grpc v1.85.0
go: toolchain upgrade needed to resolve google.golang.org/grpc
go: google.golang.org/grpc@v1.85.0 requires go >= 1.26.0 (running go 1.24.4; GOTOOLCHAIN=local)
```

والسبب أن الأمر `go mod tidy` يأخذ أحدث إصدار من مكتبة [grpc-go](https://github.com/grpc/grpc-go?ref=arabroot.io)، وهذا الإصدار يحتاج Go 1.26، بينما Go في مستودع Debian 13 هو 1.24.4، ووضعنا `GOTOOLCHAIN=local` حتى لا تنزل Go مترجماً أحدث وحدها دون علمنا. وهذا مثال صغير على مشكلة كبيرة في الأنظمة متعددة اللغات: كل منظومة لها إيقاع إصدارات مختلف، ومكتبة في منظومة واحدة قد تفرض عليك ترقية أداة في كل خطوط البناء. والحل كان أن نثبت إصداراً من gRPC يدعم Go 1.24 قبل `tidy`، وهو سطر أضفناه إلى السكربت:

```bash
go get google.golang.org/grpc@v1.72.0
```

وبعد ذلك اكتمل البناء، والمخرج سوف يكون كما يلي (أزلنا أسطر التنزيل):

```text
C: gcc shared library                 0.1 s
Go: protoc plugins (go install)      29.3 s
Go: protoc code generation            0.0 s
Go: pin grpc for Go 1.24              7.7 s
Go: go mod tidy (download deps)       1.5 s
Go: c-shared library for FFI          4.1 s
Go: HTTP + gRPC server               14.9 s
Python: grpc_tools code gen           0.1 s
  lev_pb2.py                  7.6 KB
  lev_pb2_grpc.py             2.0 KB
  levserver               16844.4 KB
  libgolev.h                  1.7 KB
  libgolev.so              1656.6 KB
  liblev.so                  15.1 KB
```

في المخرج أعلاه لاحظ التالي:

- مكتبة C بنيت في عشر الثانية، وحجمها 15.1 كيلوبايت، لأنها لا تحتاج شيئاً غير مكتبة C في النظام.
- مكتبة Go المشتركة حجمها 1.6 ميجابايت لنفس الدالة، لأنها تحمل بيئة تشغيل Go كاملة معها.
- أطول خطوة لم تكن الترجمة، وإنما تثبيت أداتي توليد الكود `protoc-gen-go` و`protoc-gen-go-grpc`، أي أن gRPC وحده أضاف أدوات لا علاقة لها بمنطق المشروع.
- خادم Go فيه HTTP وgRPC حجمه 16.8 ميجابايت، وأغلبه مكتبة gRPC واعتمادياتها.

والصورة التالية تجمع ما أضافته كل طريقة إلى المشروع:

![مقارنة تعقيد البناء: Python وحدها تحتاج مترجماً واحداً وImage بحجم 168 ميجابايت، بينما المختبر كاملاً يحتاج gcc وGo وPython وprotoc وأداتي توليد وImage بحجم 1.0 جيجابايت، وملف proto من 14 سطراً ولد 496 سطراً من كود Go، وملف go.sum فيه 34 سطراً لاعتماديات لم نكتبها، ومكتبة C بحجم 15 كيلوبايت مقابل 1.6 ميجابايت لمكتبة Go](https://arabroot.io/content/images/2026/10/polyglot-programming-04-build.webp)

14 سطراً من العقد ولدت 496 سطراً من الكود، والـ Image كبر من 168 ميجابايت إلى 1.0 جيجابايت

فالـ Image الذي يحتوي الأدوات الثلاث حجمه 1.0 جيجابايت، مقابل 168 ميجابايت لنفس النظام مع Python وحدها، وملف العقد الذي كتبناه 14 سطراً ولد 496 سطراً من كود Go، وظهر في `go.sum` أربعة وثلاثون سطراً لاعتماديات لم نخترها بأنفسنا. ولا شيء من هذا كارثي، ولكنه كله يجب أن يبنى ويحدث ويفحص من الثغرات في كل خط CI، لسنوات.

## النتائج: كم يكلف عبور الحدود بين لغتين؟

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

```text
1) cost of one call to add(a, b), in microseconds
   pure Python                       0.076 us
   C via ctypes                      0.930 us
   Go via ctypes+cgo                 1.184 us
   Go over HTTP (keep-alive)       238.378 us
   Go over gRPC                    278.807 us

2) edit distance of 500 pairs of 300-character strings
   pure Python                     12294.9 ms   x    1.0 vs Python   same answers: True
   C via ctypes                       50.5 ms   x  243.7 vs Python   same answers: True
   Go via ctypes+cgo                 107.4 ms   x  114.5 vs Python   same answers: True
   Go over HTTP, one batch           109.2 ms   x  112.6 vs Python   same answers: True
   Go over HTTP, 500 requests        245.8 ms   x   50.0 vs Python   same answers: True
   Go over gRPC, one batch           114.1 ms   x  107.7 vs Python   same answers: True
   Go over gRPC, 500 calls           271.7 ms   x   45.2 vs Python   same answers: True
```

والتشغيل الثاني أعطى أرقاماً قريبة جداً: Python في 12,356.9 ms، وC في 48.6 ms، وGo عبر FFI في 110.9 ms، وHTTP بدفعة واحدة في 104.5 ms، وgRPC بدفعة واحدة في 102.3 ms، وكلفة استدعاء `add` بين 1.1 و1.2 ميكروثانية عبر FFI وبين 257 و281 ميكروثانية عبر الشبكة. وكل الطرق أعطت نفس الإجابات، وهذا ما يقوله العمود `same answers: True`.

![كلفة استدعاء واحد لدالة لا تعمل شيئاً على مقياس لوغاريتمي: داخل Python 0.08 ميكروثانية، وعبر ctypes إلى C حوالي 0.9 ميكروثانية، وعبر ctypes إلى Go حوالي 1.2 ميكروثانية، وعبر HTTP حوالي 238 ميكروثانية، وعبر gRPC حوالي 279 ميكروثانية، أي أن عبور الشبكة أغلى من استدعاء Python بحوالي ثلاثة آلاف مرة](https://arabroot.io/content/images/2026/10/polyglot-programming-05-call-cost.webp)

كل حد تعبره يضرب الكلفة: عشرة أضعاف لـ FFI، وثلاثة آلاف ضعف للشبكة حتى على نفس الجهاز

ولنقرأ الأرقام من الأعلى:

- استدعاء دالة داخل Python يكلف 0.076 ميكروثانية، وعبور الحد إلى C عبر ctypes يكلف قرابة 0.9 ميكروثانية، أي أكثر من عشرة أضعاف، لأن ctypes تحول كل معامل من كائن Python إلى قيمة C ثم تحول النتيجة بالعكس.
- الاستدعاء إلى Go أغلى قليلاً من C، لأن cgo يبدل بين بيئتي التشغيل في كل استدعاء.
- عبور الشبكة، حتى على نفس الجهاز ومع اتصال مفتوح، يكلف بين 238 و281 ميكروثانية، أي أكثر من ثلاثة آلاف ضعف الاستدعاء المحلي. وgRPC كان أبطأ قليلاً من HTTP البسيط هنا، لأن الطلب صغير جداً وطبقات gRPC وHTTP/2 لها كلفة ثابتة.

ثم ننتقل إلى العمل الحقيقي، وهنا تتغير الصورة:

![زمن حساب المسافة لخمسمئة زوج من الكلمات: Python وحدها 12.3 ثانية، وC عبر ctypes 50 ميلي ثانية أي أسرع 244 مرة، وGo عبر FFI 107 ميلي ثانية، وGo عبر HTTP بدفعة واحدة 109 ميلي ثانية، وGo عبر gRPC بدفعة واحدة 114 ميلي ثانية، بينما 500 طلب منفصل عبر HTTP أخذت 246 ميلي ثانية و500 استدعاء gRPC أخذت 272 ميلي ثانية](https://arabroot.io/content/images/2026/10/polyglot-programming-06-work.webp)

عندما يكون العمل كبيراً تختفي كلفة الشبكة في الدفعة الواحدة، وتعود عندما تقسم العمل إلى طلبات صغيرة

- Python وحدها أخذت 12.3 ثانية، وC أخذت 50 ميلي ثانية، أي أسرع 244 مرة. وهذا هو سبب وجود FFI أصلاً: الدالة الساخنة في لغة مترجمة تغير كل شيء.
- Go عبر FFI أبطأ من C بمرتين تقريباً، والسبب الأول هو النسخ في `C.GoString` والذاكرة التي تحجزها الدالة لكل زوج ويجمعها جامع القمامة، والسبب الثاني أن Go تتحقق من حدود المصفوفات Bounds Checking عند الوصول إليها إلا حيث يثبت المترجم أن التحقق غير لازم، بينما C لا تتحقق من شيء. وهذا الثمن هو نفسه ما يجعل Go آمنة في الذاكرة.
- Go عبر HTTP أو gRPC بدفعة واحدة كانت تقريباً بنفس سرعة Go عبر FFI، لأن كلفة الشبكة، قرابة ربع ميلي ثانية، صغيرة جداً أمام 100 ميلي ثانية من العمل.
- ولكن عندما أرسلنا نفس العمل كخمسمئة طلب منفصل، تضاعف الوقت إلى 246 ميلي ثانية عبر HTTP و272 عبر gRPC، والفرق كله تقريباً هو 500 مرة كلفة الشبكة.

والدرس الأهم من هذه الأرقام هو شكل الواجهة وليس اللغة: إذا كانت الحدود بين اللغتين شبكة، فاجعل الواجهة «سميكة» Chunky، أي طلباً واحداً يحمل عملاً كثيراً، ولا تجعلها «ثرثارة» Chatty، أي طلباً لكل عنصر صغير. والخطأ المعتاد أن يكتب الفريق خدمة منفصلة ثم يستدعيها داخل حلقة `for` لكل منتج، فيضيع كل ما كسبه من السرعة. وقد ناقشنا شكل الواجهات في مقال [تصميم واجهات API](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-rest-%D9%88-grpc-%D9%88-graphql/).

وبالنسبة لفريق الشاي، الأرقام تقول إن مكتبة C داخل نفس العملية هي الأسرع، وإن خدمة Go بدفعة واحدة قريبة منها. والقرار بينهما لا تحسمه السرعة، وإنما ما سوف نراه في التجربة التالية.

### ما الذي يحدث داخل استدعاء FFI؟

قد يتساءل البعض: إذا كانت دالة C تعمل في نفس العملية ونفس الذاكرة، فلماذا يكلف استدعاؤها عشرة أضعاف استدعاء دالة Python؟ والإجابة أن اللغتين لا تتكلمان نفس «اللغة» في الذاكرة. فالرقم في Python كائن كامل له نوع وعداد مراجع، والرقم في C هو 8 بايتات فقط، والنص في Python كائن Unicode، والنص في C سلسلة بايتات تنتهي بصفر. لذلك عندما تستدعي `c.lev(a, b)` تمر ctypes بخطوات ثابتة: تقرأ `argtypes` لتعرف ماذا تتوقع الدالة، وتحول كل معامل من كائن Python إلى القيمة المناسبة في C، وتضع القيم في المسجلات Registers والـ Stack حسب اتفاقية الاستدعاء Calling Convention الخاصة بالمعالج ونظام التشغيل، وهي جزء مما يسمى واجهة الملف الثنائي Application Binary Interface واختصاراً ABI، ثم تقفز إلى الدالة، ثم تحول النتيجة حسب `restype` إلى كائن Python جديد. وكل هذا يحدث في كل استدعاء، لذلك فالكلفة ثابتة تقريباً مهما كانت الدالة صغيرة.

وهناك تفصيل مهم يذكره [توثيق ctypes](https://docs.python.org/3/library/ctypes.html?ref=arabroot.io): عند استدعاء دالة من مكتبة محملة بـ `CDLL` تحرر Python القفل العام Global Interpreter Lock واختصاراً GIL قبل الاستدعاء، ثم تأخذه بعده. وهذا يعني أن دالة C الطويلة لا توقف بقية الخيوط Threads في برنامج Python، فيمكن لعدة خيوط أن تحسب المسافة في نفس الوقت على عدة معالجات، وهذا شيء لا تستطيعه دالة Python العادية في الإصدارات التي فيها GIL، وقد شرحنا القفل العام في مقال [الخيوط Threads والأقفال](https://arabroot.io/articles/%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-threads-%D9%88%D8%A7%D9%84%D8%A3%D9%82%D9%81%D8%A7%D9%84-%D9%88-async-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/). ولكن هذا التحرير نفسه سبب آخر لأن تكون دالة C آمنة مع الخيوط، فلا تستخدم متغيرات عامة دون حماية.

وفي حالة Go الأمر أعقد، فالمكتبة `libgolev.so` تحمل معها بيئة تشغيل Go كاملة: جدولة الـ Goroutines، وجامع القمامة، والخيوط الخاصة بها. وعند كل استدعاء ينتقل التنفيذ من عالم C إلى عالم Go عبر cgo، ويتأكد أن الخيط الحالي معروف لبيئة تشغيل Go، ثم يرجع، ولهذا كانت كلفة `GoAdd` أعلى قليلاً من `add` في C. ويعني هذا أيضاً أن عملية Python عندك صار فيها جامعا قمامة: جامع Python وجامع Go، وكل منهما لا يعرف شيئاً عن ذاكرة الآخر، ولهذا تفرض cgo قواعد صارمة على تمرير المؤشرات بين اللغتين، وتنسخ `C.GoString` النص بدلاً من أن تشير إليه.

### وما الذي يحدث داخل استدعاء الشبكة؟

أما ربع الميلي ثانية في استدعاء HTTP فتتوزع على خطوات كثيرة، رغم أن الطلب لم يغادر الجهاز: تبني Python نص الطلب وترويساته Headers، ثم تكتبه في Socket باستدعاء نظام System Call، فتنقله النواة Kernel عبر واجهة الشبكة المحلية Loopback إلى Socket الخدمة، فتستيقظ Goroutine في خادم Go وتقرأ الطلب وتحلل الرابط والمعاملات، ثم تحسب الجمع، ثم تكتب الرد، فتنقله النواة مرة أخرى، ثم تقرأه Python وتحلل الترويسات وتحول النص إلى رقم. وكل استدعاء نظام هنا ينقل التنفيذ بين فضاء المستخدم وفضاء النواة، كما شرحنا في مقال [فضاء النواة واستدعاءات النظام](https://arabroot.io/articles/%D9%81%D8%B6%D8%A7%D8%A1-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D8%A9-%D9%88%D9%81%D8%B6%D8%A7%D8%A1-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%AE%D8%AF%D9%85-%D9%88%D8%A7%D8%B3%D8%AA%D8%AF%D8%B9%D8%A7%D8%A1%D8%A7%D8%AA-%D8%A7%D9%84%D9%86%D8%B8%D8%A7%D9%85/).

وفي gRPC تضاف طبقات أخرى: الرسالة تحول إلى صيغة Protocol Buffers الثنائية، وتنقل عبر بروتوكول HTTP/2 بإطاراته Frames وضغط ترويساته، وفي جانب Python تمر عبر مكتبة grpcio المكتوبة في داخلها بلغة C++. وهذه الطبقات تعطيك فوائد حقيقية في الرسائل الكبيرة والكثيرة: صيغة أصغر من JSON، وتعدد الطلبات على اتصال واحد Multiplexing، وعقد مكتوب يولد منه الكود. ولكن في رسالة فيها رقمان فقط، الكلفة الثابتة لهذه الطبقات أكبر من الفائدة، ولهذا كان gRPC أبطأ قليلاً من HTTP البسيط في مختبرنا. ولا تستنتج من هذا أن gRPC أبطأ دائماً، وإنما أن الرسائل الصغيرة جداً لا تبين قوته.

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

## العقد Contract بين لغتين: البيانات التي تعبر الحد

عندما تعبر البيانات من لغة إلى أخرى، فكل لغة تفهمها بأنواعها هي، وهنا تظهر أخطاء لا يراها أي من الطرفين وحده. ورأينا مثالين في هذه السلسلة: ضريبة مختلفة بهللة في المقال السابق لأن كل لغة قربت الرقم بطريقتها، و`add` التي أعطت رقماً سالباً لأن ctypes افترضت نوعاً أصغر. والقاعدة العامة أن الحد بين لغتين يحتاج عقداً مكتوباً يحدد الأنواع بدقة.

- الأرقام الكبيرة في JSON: معيار JSON نفسه، في [القسم 6 من RFC 8259](https://www.rfc-editor.org/rfc/rfc8259?ref=arabroot.io#section-6)، ينبه إلى أن الأرقام الصحيحة الأكبر من 253 قد لا تمثل بدقة في تطبيقات كثيرة، لأن JavaScript مثلاً تخزن كل الأرقام كأرقام Float من 64 بت. ولهذا ترجع منصة X معرفات التغريدات نصاً في الحقل `id_str` بجانب الرقم، كما تشرح في [توثيقها](https://developer.x.com/en/docs/twitter-ids?ref=arabroot.io). فإذا كانت خدمة Go ترسل معرف طلب من 64 بت إلى واجهة بلغة JavaScript، فأرسله نصاً.
- المال: اتفق بين اللغات على أن المبالغ أعداد صحيحة بالهللات، أو نصوص عشرية، وليس أرقام Float.
- الوقت: اتفق على صيغة واحدة، مثل نص ISO 8601 بتوقيت UTC، لأن كل لغة لها نوع تاريخ مختلف وافتراضات مختلفة عن المنطقة الزمنية.
- النصوص: اتفق على UTF-8 في كل الحدود، فالنص العربي الذي يعبر إلى C دون ترميز واضح سوف يصل بايتات لا تعرف C معناها.
- تطور العقد: في Protocol Buffers كل حقل له رقم، و[توثيق protobuf](https://protobuf.dev/programming-guides/dos-donts/?ref=arabroot.io) ينبه إلى ألا تعيد استخدام رقم حقل حذفته، لأن الخدمات القديمة سوف تقرأ الحقل الجديد بمعنى الحقل القديم. وفي JSON لا يوجد شيء يمنعك، فتحتاج أن تكتب أنت قواعد الإصدارات وتلتزم بها.

وأفضل حماية عملية هي اختبارات العقد Contract Tests: مجموعة واحدة من المدخلات والمخرجات المتوقعة، تشغلها على الطرفين، فإذا أعطت Python وGo نتيجة مختلفة لنفس المدخل فسوف تعرف قبل أن يعرف المحاسب. وفي مختبرنا كان العمود `same answers: True` شكلاً بسيطاً من هذه الفكرة، حيث قارنا كل طريقة بالنتيجة المرجعية من Python.

## ماذا يحدث إذا أخطأنا أو تعطل الطرف الآخر؟

كتبنا السكربت `whatif.py` لأربع حالات تحدث في الإنتاج عاجلاً أو آجلاً، والمخرج كما يلي:

```text
1) C add() without argtypes/restype (ctypes assumes C int)
   add(2, 3)                         -> 5
   add(3_000_000_000, 1)             -> -1294967295
   argtypes only: add(3_000_000_000, 1)       -> -1294967295
   argtypes + restype: add(3_000_000_000, 1)  -> 3000000001

2) a bad input reaches the C code inside our process: lev(NULL, 'abc')
   python process exit code: -11 | stdout: '' | stderr: ''

3) a bad input reaches the Go service over HTTP
   HTTP status: 400 | body: invalid character 'o' in literal null (expecting 'u')
   next request still works: [3]
4) the Go service is down
    ConnectionRefusedError: [Errno 111] Connection refused
```

ولنقرأ كل حالة:

- الحالة الأولى: بدون تعريف الأنواع تفترض ctypes أن كل شيء من نوع `int` بطول 32 بت، فيعمل `add(2, 3)` بشكل صحيح ويخدعك، ثم يعطي `add(3_000_000_000, 1)` رقماً سالباً دون أي خطأ أو تحذير. ولاحظ السطر الثالث: حتى بعد تعريف المدخلات بقيت النتيجة خاطئة، لأن نوع المخرج `restype` ما زال `int`، ولم تصح النتيجة إلا بعد تعريف الاثنين. وهذا النوع من الأخطاء هو الأخطر، لأنه لا يظهر في الاختبارات بالأرقام الصغيرة، ويظهر في الإنتاج عندما يتجاوز رقم الطلب مليارين.
- الحالة الثانية: مررنا `None`، أي مؤشراً فارغاً NULL، إلى دالة C، فحاولت `strlen` قراءة العنوان صفر، وأوقف النظام العملية كلها بالإشارة SIGSEGV، وهذا معنى رمز الخروج `-11`. لاحظ أنه لا يوجد أي استثناء Exception في Python ولا أي رسالة، فالعملية ماتت فقط، ومعها كل الطلبات الأخرى التي كانت تخدمها. وقد شرحنا لماذا تسمح C بهذا في مقال [أمان الذاكرة Memory Safety](https://arabroot.io/articles/%D8%A7%D9%85%D8%A7%D9%86-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-memory-safety-%D9%84%D9%85%D8%A7%D8%B0%D8%A7-%D8%AA%D8%AF%D9%81%D8%B9-%D8%A7%D9%84%D8%AD%D9%83%D9%88%D9%85%D8%A7%D8%AA-%D9%86%D8%AD%D9%88-rust/).
- الحالة الثالثة: أرسلنا نصاً ليس JSON إلى خدمة Go، فردت بالرمز 400 ورسالة واضحة، ولاحظ أن المحلل قرأ الحرف `n` في `not json` فظن أنها بداية الكلمة `null` ثم وجد `o` بدلاً من `u`. والأهم هو السطر التالي: الطلب الصحيح بعده عمل، فالخطأ بقي داخل طلب واحد.
- الحالة الرابعة: أوقفنا الخدمة، فحصل العميل على `ConnectionRefusedError` واضح يمكن التقاطه، وعملية Python نفسها بقيت تعمل. ولكن هذا يعني أن عليك الآن كتابة مهلة Timeout وإعادة محاولة Retry وخطة لما يحدث عندما تكون الخدمة متوقفة، وهي مواضيع شرحناها في مقال [كيف تمنع خدمة بطيئة من إسقاط نظامك](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D9%85%D9%86%D8%B9-%D8%AE%D8%AF%D9%85%D8%A9-%D8%A8%D8%B7%D9%8A%D8%A6%D8%A9-%D9%85%D9%86-%D8%A7%D8%B3%D9%82%D8%A7%D8%B7-%D9%86%D8%B8%D8%A7%D9%85%D9%83-%D9%83%D9%84%D9%87/).

![حدود الأعطال: داخل نفس العملية عبر FFI خطأ تعريف الأنواع يعطي رقماً خاطئاً بصمت، ومؤشر فارغ إلى C يقتل عملية Python كلها برمز الخروج سالب 11، بينما عبر الشبكة المدخل السيئ يعطي الرمز 400 ويبقى الخادم يعمل، وتوقف الخدمة يعطي خطأ اتصال يمكن التقاطه، ولكنه يحتاج مهلة وإعادة محاولة](https://arabroot.io/content/images/2026/10/polyglot-programming-07-failures.webp)

FFI أسرع، ولكن الخطأ فيه يسقط العملية كلها، والشبكة أبطأ، ولكن الخطأ يبقى خلفها

وهنا يتضح القرار الحقيقي لفريق الشاي: FFI يعطيك السرعة القصوى، ولكنه يضع كود لغة أخرى داخل عمليتك، فأي خطأ ذاكرة فيه يصبح عطلاً في خدمتك كلها، والخدمة المنفصلة أبطأ بكلفة الشبكة، ولكنها تعطيك حد أعطال Fault Isolation، فتعطلها لا يسقط خدمة البحث، ويمكنك أن تعيد تشغيلها وتحدثها وتراقبها وحدها. وإذا اخترت FFI فاختر لغة آمنة في الذاكرة على الطرف الآخر، أو تحقق من كل مدخل قبل أن يعبر الحد.

## من يصون اللغة الثانية؟ الفريق والتشغيل Operations

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

ولذلك إذا قررت إضافة لغة فاكتب معها ثلاثة أشياء: الأول اسم من يملك هذا الجزء ومن يحل محله، والثاني دليلاً قصيراً لبنائه وتشغيله واختباره محلياً، حتى لا تبقى المعرفة في رأس شخص واحد، والثالث المراقبة الخاصة به. فالخدمة المنفصلة تحتاج سجلات Logs ومقاييس Metrics وتنبيهات مثل أي خدمة، وقد شرحنا ذلك في مقال [مفاهيم المراقبة Observability](https://arabroot.io/articles/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%A7%D9%84%D9%85%D8%B1%D8%A7%D9%82%D8%A8%D8%A9-observability-%D9%85%D9%86-%D8%A7%D9%84%D8%A7%D8%B4%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%AA%D9%86%D8%A8%D9%8A%D9%87/)، أما مكتبة FFI فتحتاج طريقة لترى ما يحدث داخلها، لأن عطلها يظهر كموت مفاجئ للعملية دون أي سطر في سجلات Python، كما رأيت في الحالة الثانية من التجربة.

وفي فرق المناوبة On-Call يظهر الأثر بوضوح: الشخص الذي يستيقظ ليلاً بسبب تنبيه يجب أن يعرف أين يبحث، وكلما زاد عدد اللغات زادت الأدوات التي يجب أن يعرفها، من منقح Python إلى أدوات تحليل الذاكرة في C إلى أدوات Go لتحليل الأداء. وهذه تكلفة لا تظهر في أي مقارنة أداء، ولكنها تظهر في سرعة إصلاح الأعطال، وفي رضا الفريق عن عمله.

## قصتان حقيقيتان: Discord ومكتبة cryptography

القصة الأولى من Discord، وهي مثال على Polyglot ناجح. نشرت الشركة في مايو 2019 مقالاً بعنوان [Using Rust to Scale Elixir for 11 Million Concurrent Users](https://discord.com/blog/using-rust-to-scale-elixir-for-11-million-concurrent-users?ref=arabroot.io). كانت خدمتها مكتوبة بلغة Elixir، واحتاجت هيكل بيانات مرتباً يحمل مئات الآلاف من أعضاء الخادم ويتغير باستمرار، ولأن هياكل البيانات في Elixir غير قابلة للتعديل Immutable فإن كل تغيير يبني نسخة جديدة. وجرب الفريق عدة هياكل في Elixir نفسها، وأفضلها وصل إلى قرابة 640 ميكروثانية لإضافة عنصر في نهاية مجموعة من 250,000 عنصر. ثم كتبوا الهيكل بلغة Rust كدالة أصلية Native Implemented Function واختصاراً NIF، وهي شكل FFI في بيئة Erlang، باستخدام مكتبة Rustler، فصار أسوأ زمن لإضافة عنصر قرابة 3.68 ميكروثانية في مجموعات حتى مليون عنصر، والوحدة تظهر لبقية الكود كأي وحدة Elixir عادية.

ولاحظ ثلاثة أشياء في هذه القصة: اللغة الثانية دخلت في هيكل بيانات واحد محدد وليس في النظام كله، واختاروا Rust وهي لغة آمنة في الذاكرة على الطرف الآخر من FFI، ويذكر المقال أن عملياتهم بقيت أقل بكثير من ميلي ثانية واحدة، وهذا مهم لأن الدالة الأصلية التي تطول تعطل جدولة بيئة Erlang نفسها.

والقصة الثانية تبين الوجه الآخر. ففي فبراير 2021 أصدرت مكتبة [cryptography](https://cryptography.io/en/latest/changelog/?ref=arabroot.io)، وهي من أكثر مكتبات Python استخداماً، الإصدار 3.4 الذي يضم كوداً بلغة Rust، وكتبت في سجل التغييرات أن من يبني المكتبة بنفسه يحتاج Rust. ومن يثبت الحزم الجاهزة Wheels لم يلاحظ شيئاً، ولكن في نفس الأسبوع فتح أحد مطوري توزيعة Gentoo [النقاش رقم 5771](https://github.com/pyca/cryptography/issues/5771?ref=arabroot.io) بعنوان «الاعتماد على Rust يزيل الدعم عن عدد من المنصات»، وتوالت الشكاوى: خطوط CI على Alpine Linux التي لا تجد حزمة جاهزة فتبني من المصدر وتفشل لعدم وجود Rust، ومعماريات معالجات لا يدعمها Rust، وتوزيعات لا تسمح سياساتها بتضمين مكتبات Rust بالطريقة المطلوبة، ومستخدمون بإصدارات قديمة من pip. ولجأ كثيرون إلى تثبيت الإصدار السابق، وأغلق النقاش لأنه صار حاداً جداً، ثم أضاف الفريق رسائل أوضح وخفض الإصدار الأدنى المطلوب من Rust، وأصدر حزماً جاهزة لـ musl في سبتمبر 2021.

والدرس من هذه القصة لا يخص Rust، وإنما يخص أي لغة ثانية: عندما تضيف لغة إلى مكتبة أو نظام، فأنت لا تضيفها لنفسك فقط، وإنما لكل من يبني الكود من المصدر، ولكل خط CI ولكل منصة يعمل عليها. وكانت حجة الفريق، أمان الذاكرة في كود التشفير، حجة قوية، ولكن الثمن دفعه أشخاص لم يكونوا في النقاش.

## جدول القرار Decision Table

| حالتك                                               | الطريقة المناسبة                                  | انتبه إلى                                                     |
| --------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------- |
| دالة ساخنة صغيرة، والمدخلات بسيطة، وتحتاج أقصى سرعة | FFI بلغة آمنة في الذاكرة (Rust) أو C مع تحقق صارم | تعريف الأنواع، وأي خطأ يسقط العملية، وأدوات البناء في كل مكان |
| مكتبة موجودة في لغة أخرى ولا تريد إعادة كتابتها     | خدمة منفصلة بواجهة سميكة                          | كلفة الشبكة لكل طلب، والمهلة وإعادة المحاولة                  |
| فريق مستقل يملك خدمته كاملة                         | خدمة بلغته عبر gRPC أو HTTP مع عقد واضح           | توليد الكود، وإصدارات العقد، والتتبع الموزع                   |
| تريد لغة أحدث دون تغيير بيئة التشغيل                | لغة على نفس الـ Runtime، مثل Kotlin مع Java       | أرخص الطرق، فابدأ بها إن أمكن                                 |
| الجزء البطيء يستدعى مرة لكل عنصر صغير               | غير الواجهة إلى دفعات قبل أن تغير اللغة           | الواجهة الثرثارة تضيع مكسب أي لغة                             |
| فريق صغير، والبطء محتمل، ولا أحد يعرف اللغة الثانية | ابق بلغة واحدة وحسن الخوارزمية أولاً              | كل لغة جديدة تكلف سنوات من الصيانة                            |

وفي قصتنا، قرر فريق الشاي أن يبدأ بتحسين الخوارزمية في Python نفسها، لأن أغلب المقارنات يمكن تجنبها بفهرس للكلمات المتقاربة، وقد شرحنا ذلك في مقال [البحث في النصوص العربية](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%AD%D8%AB-%D9%81%D9%8A-%D8%A7%D9%84%D9%86%D8%B5%D9%88%D8%B5-%D8%A7%D9%84%D8%B9%D8%B1%D8%A8%D9%8A%D8%A9-%D9%85%D8%B9-opensearch-%D9%88-meilisearch/). فإذا بقيت المشكلة بعد ذلك، فالخيار الأقرب هو مكتبة مكتوبة بلغة Rust أو C موجودة أصلاً على PyPI، يصونها غيرهم، بدلاً من كتابة FFI بأنفسهم.

## أخطاء شائعة Common Mistakes

- إضافة لغة لأن جزءاً «قد يكون بطيئاً»: قس أولاً، فقد يكون البطء في الخوارزمية أو في قاعدة البيانات وليس في اللغة.
- واجهة ثرثارة عبر الشبكة: رأيت في المختبر أن نفس العمل تضاعف وقته عندما قسمناه إلى 500 طلب.
- نسيان `argtypes` و`restype` في ctypes: النتيجة الخاطئة تظهر فقط مع الأرقام الكبيرة.
- تمرير مدخلات غير متحقق منها إلى C: مؤشر فارغ واحد أسقط عملية Python كلها دون رسالة.
- تكرار قواعد العمل في لغتين: اجعل القاعدة في مكان واحد، أو اكتب اختبارات مشتركة تتحقق من أن اللغتين تعطيان نفس النتيجة.
- عدم تثبيت إصدارات الأدوات: `go mod tidy` أخذ إصدار gRPC يحتاج مترجماً أحدث من الذي عندنا.
- تجاهل من يبني من المصدر: قصة مكتبة cryptography تبين أن اللغة الثانية تصل إلى كل من يبني الكود، لا إليك وحدك.
- غياب المهلة وإعادة المحاولة عند الاتصال بخدمة بلغة أخرى: توقف الخدمة سوف يحدث، والسؤال فقط متى.

## قائمة تحقق Checklist قبل أن تضيف لغة

- هل قست أن المشكلة في اللغة فعلاً، وليست في الخوارزمية أو في الإدخال والإخراج؟
- هل يوجد في الفريق شخصان على الأقل يفهمان اللغة الجديدة؟
- أي شكل تقصد: خدمة منفصلة، أم FFI، أم لغة على نفس بيئة التشغيل؟
- هل الواجهة بين اللغتين سميكة، أي دفعات وليس عنصراً عنصراً؟
- إذا كان FFI، هل الطرف الآخر آمن في الذاكرة، وهل تعريف الأنواع مكتوب ومختبر بأرقام كبيرة؟
- إذا كانت خدمة، هل كتبت المهلة وإعادة المحاولة والتتبع والمراقبة؟
- هل أضفت الأدوات الجديدة إلى كل خط CI، وثبت إصداراتها، وأضفت فحص الثغرات لمنظومتها؟
- هل توجد قواعد عمل مكررة بين اللغتين، ومن يضمن أنها متطابقة؟
- هل كتبت القرار وأسبابه في وثيقة ADR، ومتى تراجعه؟

## حذف المختبر Cleanup

كل خطوات المختبر عملت في Containers مؤقتة بالخيار `--rm`، فيكفي حذف الـ Image والملفات المولدة:

```bash
docker rmi scp6-poly
rm -rf go/levpb
```

## الخلاصة

- البرمجة بعدة لغات أربعة أشكال: خدمات عبر الشبكة، وFFI داخل نفس العملية، ولغات على بيئة تشغيل واحدة، وواجهة وخادم، ولكل شكل كلفة مختلفة.
- اللغة الثانية تفيد عندما تملك مكتبات لا تملكها لغتك، أو عندما تكون هناك دالة ساخنة محددة مقاسة، وتضر عندما تضاف لسبب غامض.
- في مختبرنا كانت C عبر ctypes أسرع من Python بحوالي 244 مرة في العمل الحقيقي، وكلفة عبور FFI قرابة ميكروثانية، وكلفة الشبكة قرابة ربع ميلي ثانية للاستدعاء.
- الدفعة الواحدة عبر الشبكة كانت بسرعة FFI تقريباً، و500 طلب صغير ضاعفت الوقت، فالواجهة السميكة أهم من اللغة.
- FFI يضع أخطاء اللغة الأخرى داخل عمليتك: تعريف أنواع ناقص أعطى رقماً خاطئاً بصمت، ومؤشر فارغ أسقط Python كلها، بينما بقيت الأخطاء خلف الشبكة محصورة في طلب واحد.
- تعقيد البناء حقيقي: Image من 168 ميجابايت إلى 1.0 جيجابايت، وأدوات توليد كود، وإصدار مكتبة فرض مترجماً أحدث.

وإذا لم تقرأ المقال السابق فابدأ بمقال [كيف تختار لغة البرمجة لمشروعك؟](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%84%D8%BA%D8%A9-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%84%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/)، ثم [تصميم واجهات API بين REST وgRPC](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-rest-%D9%88-grpc-%D9%88-graphql/) لتختار شكل الواجهة بين خدماتك، و[أمان الذاكرة](https://arabroot.io/articles/%D8%A7%D9%85%D8%A7%D9%86-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-memory-safety-%D9%84%D9%85%D8%A7%D8%B0%D8%A7-%D8%AA%D8%AF%D9%81%D8%B9-%D8%A7%D9%84%D8%AD%D9%83%D9%88%D9%85%D8%A7%D8%AA-%D9%86%D8%AD%D9%88-rust/) قبل أن تكتب أي FFI، و[أدوات البناء Build Tools](https://arabroot.io/articles/%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D9%86%D8%A7%D8%A1-build-tools-%D9%85%D9%86-make-%D8%A7%D9%84%D9%89-maven-%D9%88-gradle-%D9%88-dotnet/) لتفهم ما يحدث في كل خطوة من خطوات البناء التي قسناها.

## سجل التحديثات

- أكتوبر 2026: كتابة المقال واختبار الطرق الخمس على Python 3.13.5 وgcc 14.2.0 وGo 1.24.4 مع grpc-go 1.72.0 وgrpcio 1.51.1 وprotoc 3.21.12.