ارتباط میان‌زبانی

زبان مورد استفاده برای پیاده‌سازی یک ابزار، یکی از ویژگی‌های آن نیست.

دنیا پر از ابزارها و کتابخانه‌هایی هست که به‌دست برنامه‌نویسان مختلفی به‌زبان‌های مختلف نوشته شدن. بدون وجود این کتابخانه‌ها و ابزارها زندگی برای ما برنامه‌نویس‌ها (به‌دلایلی واضح) خیلی سخت می‌شد.

به عقیدهٔ من همه‌چیز باید همه‌جا برای همه‌کس قابل استفاده باشه. یعنی این که مثلاً این که من فلان کتابخانهٔ کاربردی و باحال رو با زبان ‪C++‬ پیاده‌سازی کردم نباید باعث بشه که یک برنامه‌نویس پایتون یا جاوا نتونه ازش استفاده کنه. حتا زبان‌هایی که دامنه و کاربرد مختلفی دارن باید پشتیبانی بشن. مثلاً یکی از ابزارهایی که ساختم در اصل به‌عنوان یک «حل‌کنندهٔ مسأله» برای کاربردهای پیشرفتهٔ هوش مصنوعی طراحی شده، و این‌چنین موضوعاتی معمولاً برای کاربردهای سیستمی و خاص‌منظوره استفاده میشن. اما هیچ دلیلی وجود نداره که یک برنامه‌نویس وب برای یک اپلیکیشن آنلاین نخواد از مدل‌سازی ارضای محدودیت برای حل یک سری مسائل داخل برنامه‌ش استفاده کنه، یا یک برنامه‌نویس اندرویید نخواد از سیستم‌های استنتاج فازی برای برنامه‌ش استفاده کنه. بنابراین وظیفهٔ منِ برنامه‌نویس است، که برای تمام زبان‌هایی که می‌تونم، رابط (=>interface)های بومی (=>native) فراهم کنم تا همه بتونن از ابزارم استفاده کنن.

توی این پست نحوهٔ ایجاد رابط برای زبان‌های مختلف رو توضیح میدم. طبق این روش ساده، میشه به‌راحتی برای کتابخانه‌های ‪C++‬ رابط‌هایی برای تمام زبان‌های دیگه پیاده‌سازی کرد.

فلسفهٔ یونیکس میگه که هر ابزار باید یک کار رو به شکلی خوب انجام بده. همچنین از نظر یونیکس چیزهایی که کارهایی رو به‌شکلی خوب انجام میدن با خط‌لوله(=>فارسی‌شدهٔ پایپ) با هم ترکیب میشن تا کارهای بزرگتر رو انجام بدن. از این فلسفه خیلی خوشم میاد و سعی کردم توی تمام چیزهایی که می‌سازم پیاده‌سازی‌ش کنم. با این حال اگر با طرز فکری مشابه بخوام ارتباط بین ابزارها رو تأمین کنم، باید رابط‌های خط‌فرمان خیلی سخت (و نه‌پیچیده)‌ای رو پیاده‌سازی کنم. که البته این کار رو هم کردم (: اما ارتباطی که من ازش صحبت می‌کنم در ردهٔ پایین‌تری هست. یعنی نمی‌خوام که یک برنامهٔ مجزا هر بار فراخوانی بشه و کلی ورودی بگیره و کلی هم خروجی چاپ کنه و یک سری پایپ‌هایی متوالی برای پردازش اطلاعات استفاده بشه (باوجود این که این کار خیلی زیبا هست). در عوض می‌خوام در سطح پایین‌تر، کتابخانهٔ مشترکی که نوشتم، به شکل یک API برای هر زبان برنامه‌نویسی هدف در دسترس باشه. برای این کار باید APIهای بومی هر زبان پیاده‌سازی بشه که در پشت صحنه از فراخوانی‌های کتابخونهٔ اصلی استفاده می‌کنن.

اما این کار چطور ممکنه؟ مشکلات زیادی وجود داره. اول این که ABIی هر زبان فرق با زبان‌های دیگه متفاوته. دوم این که خیلی از زبان‌ها (از جمله زبان‌های مفسری) اصلا ABI ندارن. کلاً باینری ندارن که ساختار باینری تعریف‌شده داشته باشند. سومین مشکل هم اینه که هر زبانی یک مجموعهٔ مشخصی از ویژگی‌ها داره که با زبان دیگه همخوانی نداره. حتا با فرض همخوانی ABI نمیشه روی یک رابط مشخص بین زبان‌ها توافق کرد. مثلاً بین زبان‌های شی‌گرا، پیاده‌سازی شی‌گرایی خیلی تنوع داره. پس چیزی که من بهش توی ‪C++‬میگم کلاس یا اینترفیس با تعریف یک برنامه‌نویس جاوا فرق داره. حتا گیریم ساختارهای باینری این دو زبان یکی باشن (که نیستند)، من چطور باید یک رابط برنامه‌نویسی برای جاوا فراهم کنم؟

در واقع باید یک همچین ساختاری وجود داشته باشه:

که در اون The Magic یک رابط باینری جهانی و استاندارد هست که تمام زبان‌های برنامه‌نویسی مجبور هستند به نحوی پیاده‌سازی‌ش کنن. خوشبختانه این رابط نه تنها وجود داره، بلکه رابطِ یک زبان برنامه‌نویسی استاندارد هم هست و اون زبان بی‌شک چیزی نیست جز:

The C Programming Language

باید یک روشی پیدا کنم که رابط برنامه‌نویسی ‪C++‬ رو به C پورت کنم و بعد از اون تمام زبان‌ها می‌تونن wrapperهایی به‌شکل بومی و با استفاده از رابط Cی کتابخونه‌ای که ساختم استفاده کنن. در واقع میشه این:

خوب برای پورت کردن کد سی++ به سی لازمه که تمام کلاس‌ها و namespaceها رو از بین ببریم. برای این کار باید همه‌چیز به سبک شی‌گرایی سی پیاده‌سازی بشه. به این صورت که یک مجموعه از توابع داریم، که آرگومان اول همه‌شون، اشاره‌گری به شی‌ای هست که قصد داریم متدی از اون رو فراخوانی کنیم. اسم این ایده PIMPL idiom هست. و به استفاده از این ایده برای طراحی رابط C میگن C wrapper for C++ API یک مثال می‌تونه خیلی مفید باشه. این کلاس ‪C++‬‬ رو در نظر بگیرید:

// foo.hpp
class Foo {
  public:
    explicit Foo( std::string & s );
    ~Foo();
    int bar( int j );
  private:
    void notYourBusiness();
}
برای این کلاس رابط C داخل یک هدر مجزا برای زبان C به این شکل پیاده‌سازی میشه:

// foo.h
#ifdef __cplusplus
extern "C"
{
#endif
struct Foo_Type; // An opaque type
typedef struct Foo_Type Foo_Type;
Foo_Type* Foo_create( const char * s );
void Foo_destroy( Foo_Type * v );
int Foo_bar( Foo_Type * v, int i );
#ifdef __cplusplus
}
#endif
و پیاده‌سازی این رابط، داخل سورس ‪C++‬ همون کلاس (یا هر جای دیگه‌ای) به این شکل انجام میشه:

// foo.cpp (or foo_c.cpp)
Foo_Type* Foo_create(const char * s) {
    Foo_Type* ms = NULL;
    try { /* ممکنه سازندهٔ رشته استثنا صادر کنه*/
        ms = new Foo(s);
    } catch (...) {}
    return static_cast<void*>( ms );
}

void Foo_destroy(Foo_Type* v) {
    Foo * ms = static_cast<Foo*>(v);
    delete ms;
}

int Foo_bar(Foo_Type* v, int i) {
    Foo * ms = static_cast<Foo*>(v);
    int ret_value = -1; /* با فرض این که منفی به معنی خطا باشه */
    try {
        ret_value = ms->bar(i);
    } catch (...) {}
    return ret_value;
}
خوب حالا یک برنامهٔ سی می‌تونه به‌راحتی با لینک کردن به این کتابخونه و ‪#include <foo.h>‬ به‌راحتی از تمام ویژگی‌های کلاس Foo استفاده کنه:

#include <foo.h>
Foo_Type* foo = Foo_create("my foo");
int i = Foo_bar(foo);
// Thins happen
Foo_destroy(foo);
فقط برنامه‌نویس C باید حواسش باشه که هدرهای ‪C++‬ رو (که هیچ ربطی به اون ندارن) وارد برنامه‌ش نکنه. برای این که از این اتفاق جلوگیری کنیم، داخل هدرهای ‪C++‬ مطمئن میشیم که کامپایلر ‪C++‬ در حال اجراست. در غیر این صورت یک خطای زمان کامپایل صادر می‌کنیم. این‌طوری:

// foo.hpp
#ifndef __cplusplus
#error "This header is a C++ header and it cannot be used via a C compiler.
#endif
از طرف دیگه یه برنامه‌نویس پایتون (یا هر زبان دیگه‌ای) می‌تونه یک رابط برای این کد بنویسه، طوری که کاربر نهایی (منظورم برنامه‌نویس پایتون نهایی هست) اصلا متوجه نشه که برنامه با ‪C++‬ نوشته شده و پایتون داره از رابط Cی اون استفاده می‌کنه:

import ctypes.util

loadName = ctypes.util.find_library('foo') # ‮ ‫مثلاً ‪‬‪libfoo.so.1.0.0‬‬ رو برمی‌گردونه‬‬
lib = ctypes.cdll.LoadLibrary(loadName)

class Foo(object):
    def __init__(self, s):
        self.obj = lib.Foo_create(s)
        
    def __del__(self):
        lib.Foo_destroy(self.obj)
        
    def solve(self, input):
        return lib.Foo_bar(self.obj, input)
و البته کمی حرفه‌ای‌تر از اینی که من مثال زدم ‪:P‬ نکتهٔ اصلی اینه که تمام این کارها بدون کوچکترین سرباری انجام میشه و کدهای فراخوانی شده کدهای ماشین هستند.

Comments

comments powered by Disqus