1. تعبیه پایتون در برنامه‌ای دیگر

فصل‌های قبلی درباره‌ی چگونگی توسعه دادن پایتون بحث کردند، یعنی چگونگی توسعه قابلیت‌های پایتون با ضمیمه کردن یک کتابخانه از توابع C به آن. انجام این کار به روش معکوس نیز ممکن است: برنامه‌ی C/C++ خود را با تعبیه پایتون در آن غنی کنید. تعبیه به برنامه‌ی شما این امکان را می‌دهد که بخشی از قابلیت‌های برنامه‌ی خود را به جای C یا C++ در پایتون پیاده‌سازی کنید. از این می‌توان برای اهداف بسیاری استفاده کرد؛ برای مثال، می‌توان به کاربران اجازه داد با نوشتن چند اسکریپت به زبان پایتون، برنامه را مطابق نیازهایشان سفارشی کنند. اگر نوشتن بخشی از قابلیت‌ها در پایتون آسان‌تر باشد، خودتان نیز می‌توانید از آن استفاده کنید.

تعبیه پایتون شبیه به توسعه دادن آن است، اما نه دقیقاً. تفاوت این است که وقتی پایتون را توسعه می‌دهید، برنامه اصلی اپلیکیشن همچنان مفسر پایتون است، در حالی که اگر پایتون را تعبیه کنید، برنامه اصلی ممکن است هیچ ربطی به پایتون نداشته باشد --- در عوض، بخش‌هایی از اپلیکیشن گاهی مفسر پایتون را فراخوانی می‌کنند تا مقداری کد پایتون را اجرا کنند.

پس اگر پایتون را تعبیه می‌کنید، برنامه اصلی خودتان را فراهم می‌کنید. یکی از کارهایی که این برنامه اصلی باید انجام دهد، مقداردهی اولیه مفسر پایتون است. در حداقل حالت، باید تابع Py_Initialize() را فراخوانی کنید. فراخوانی‌های اختیاری‌ای نیز وجود دارند که آرگومان‌های خط فرمان را به پایتون منتقل می‌کنند. سپس بعداً می‌توانید مفسر را از هر بخشی از برنامه فراخوانی کنید.

راه‌های مختلفی برای فراخوانی مفسر وجود دارد: می‌توانید رشته‌ای حاوی دستورهای پایتون را به PyRun_SimpleString() ارسال کنید، یا می‌توانید اشاره‌گر پرونده‌ی stdio و نام یک پرونده (فقط برای شناسایی در پیام‌های خطا) را به PyRun_SimpleFile() ارسال کنید. همچنین می‌توانید برای ساخت و استفاده از اشیای پایتون، عملیات‌های سطح پایین‌تری را که در فصل‌های قبلی توضیح داده شده‌اند فراخوانی کنید.

همچنین ملاحظه نمائید

راهنمای مرجع Python/C API

جزئیات رابط C پایتون در این راهنما آمده است. اطلاعات ضروری فراوانی را می‌توان در اینجا یافت.

1.1. تعبیه در سطح بسیار بالا

ساده‌ترین شکل تعبیه پایتون، استفاده از رابط سطح بسیار بالا است. این رابط برای اجرای یک اسکریپت پایتون بدون نیاز به تعامل مستقیم با برنامه در نظر گرفته شده است. برای مثال، می‌توان از این برای انجام عملیاتی روی یک پرونده استفاده کرد.

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int
main(int argc, char *argv[])
{
    PyStatus status;
    PyConfig config;
    PyConfig_InitPythonConfig(&config);

    /* optional but recommended */
    status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
    if (PyStatus_Exception(status)) {
        goto exception;
    }

    status = Py_InitializeFromConfig(&config);
    if (PyStatus_Exception(status)) {
        goto exception;
    }
    PyConfig_Clear(&config);

    PyRun_SimpleString("from time import time,ctime\n"
                       "print('Today is', ctime(time()))\n");
    if (Py_FinalizeEx() < 0) {
        exit(120);
    }
    return 0;

  exception:
     PyConfig_Clear(&config);
     Py_ExitStatusException(status);
}

توجه

#define PY_SSIZE_T_CLEAN برای نشان دادن این نکته استفاده می‌شد که در برخی API‌ها باید به‌جای int از Py_ssize_t استفاده شود. از پایتون 3.13 به بعد دیگر لازم نیست، اما برای سازگاری با نسخه‌های پیشین، آن را اینجا نگه می‌داریم. برای شرح این ماکرو به رشته‌ها و بافرها مراجعه کنید.

تنظیم PyConfig.program_name باید پیش از Py_InitializeFromConfig() انجام شود تا مفسر را از مسیرهای کتابخانه‌های زمان اجرای پایتون آگاه کند. سپس، مفسر پایتون با Py_Initialize() مقداردهی اولیه می‌شود و در ادامه، یک اسکریپت پایتونِ هاردکد‌شده (hard-coded) که تاریخ و زمان را چاپ می‌کند، اجرا می‌شود. پس از آن، فراخوانی Py_FinalizeEx() مفسر را متوقف می‌کند و در نهایت برنامه به پایان می‌رسد. در یک برنامه‌ی واقعی، ممکن است بخواهید اسکریپت پایتون را از منبع دیگری دریافت کنید؛ مثلاً از یک روتین ویرایشگر متن، یک پرونده یا یک پایگاه داده. گرفتن کد پایتون از یک پرونده را بهتر می‌توان با استفاده از تابع PyRun_SimpleFile() انجام داد که شما را از دردسر تخصیص فضای حافظه و بارگذاری محتوای پرونده بی‌نیاز می‌کند.

1.2. فراتر از تعبیه در سطح بسیار بالا: مروری کلی

رابط سطح بالا به شما امکان می‌دهد قطعات دلخواهی از کد پایتون را از درون برنامه‌تان اجرا کنید، اما تبادل مقادیر داده، دست‌کم بگوییم، بسیار پرزحمت است. اگر چنین چیزی می‌خواهید، باید از فراخوانی‌های سطح پایین‌تر استفاده کنید. به بهای اینکه ناچار شوید کد C بیشتری بنویسید، می‌توانید تقریباً هر کاری را انجام دهید.

باید توجه داشت که توسعه دادن پایتون و تعبیه کردن پایتون، با وجود تفاوت در هدف، تقریباً یک فعالیت واحد هستند. بیشتر مباحثی که در فصل‌های پیشین بحث شده‌اند، همچنان معتبرند. برای نشان دادن این موضوع، در نظر بگیرید که کد توسعه‌ای از پایتون به C در واقع چه کاری انجام می‌دهد:

  1. تبدیل مقادیر داده از پایتون به C،

  2. با استفاده از مقادیر تبدیل‌شده، فراخوانی تابعی به یک روتین C را انجام دهید، و

  3. مقادیر داده‌ی حاصل از فراخوانی را از C به پایتون تبدیل کنید.

هنگام تعبیه پایتون، کد رابط کار‌های زیر را انجام می‌دهد:

  1. تبدیل مقادیر داده از C به پایتون،

  2. با استفاده از مقادیر تبدیل‌شده، یک فراخوانی تابع به یک روتین رابط پایتون انجام دهید و

  3. مقادیر داده را از فراخوانی، از پایتون به C تبدیل کنید.

همان‌طور که می‌بینید، گام‌های تبدیل داده صرفاً برای سازگاری با جهت متفاوتِ انتقال بین دو زبان جابه‌جا شده‌اند. تنها تفاوت، روالی است که بین هر دو تبدیل داده فراخوانی می‌کنید. هنگام توسعه دادن، یک روال C را فراخوانی می‌کنید؛ هنگام تعبیه، یک روال پایتون را.

این فصل درباره‌ی نحوه‌ی تبدیل داده‌ها از پایتون به C و برعکس بحث نمی‌کند. همچنین، فرض بر این است که استفاده‌ی صحیح از ارجاع‌ها و برخورد با خطاها را درک کرده‌اید. از آنجا که این جنبه‌ها با توسعه‌ی مفسر تفاوتی ندارند، می‌توانید برای اطلاعات مورد نیاز به فصل‌های پیشین مراجعه کنید.

1.3. تعبیه خالص (Pure Embedding)

هدف اولین برنامه، اجرای یک تابع در یک اسکریپت پایتون است. مانند بخش مربوط به رابط سطح بسیار بالا، مفسر پایتون مستقیماً با برنامه تعامل نمی‌کند (اما این موضوع در بخش بعدی تغییر خواهد کرد).

کد اجرای تابع تعریف‌شده در یک اسکریپت پایتون چنین است:

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int
main(int argc, char *argv[])
{
    PyObject *pName, *pModule, *pFunc;
    PyObject *pArgs, *pValue;
    int i;

    if (argc < 3) {
        fprintf(stderr,"Usage: call pythonfile funcname [args]\n");
        return 1;
    }

    Py_Initialize();
    pName = PyUnicode_DecodeFSDefault(argv[1]);
    /* Error checking of pName left out */

    pModule = PyImport_Import(pName);
    Py_DECREF(pName);

    if (pModule != NULL) {
        pFunc = PyObject_GetAttrString(pModule, argv[2]);
        /* pFunc is a new reference */

        if (pFunc && PyCallable_Check(pFunc)) {
            pArgs = PyTuple_New(argc - 3);
            for (i = 0; i < argc - 3; ++i) {
                pValue = PyLong_FromLong(atoi(argv[i + 3]));
                if (!pValue) {
                    Py_DECREF(pArgs);
                    Py_DECREF(pModule);
                    fprintf(stderr, "Cannot convert argument\n");
                    return 1;
                }
                /* pValue reference stolen here: */
                PyTuple_SetItem(pArgs, i, pValue);
            }
            pValue = PyObject_CallObject(pFunc, pArgs);
            Py_DECREF(pArgs);
            if (pValue != NULL) {
                printf("Result of call: %ld\n", PyLong_AsLong(pValue));
                Py_DECREF(pValue);
            }
            else {
                Py_DECREF(pFunc);
                Py_DECREF(pModule);
                PyErr_Print();
                fprintf(stderr,"Call failed\n");
                return 1;
            }
        }
        else {
            if (PyErr_Occurred())
                PyErr_Print();
            fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
        }
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
    }
    else {
        PyErr_Print();
        fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
        return 1;
    }
    if (Py_FinalizeEx() < 0) {
        return 120;
    }
    return 0;
}

این کد با استفاده از argv[1] یک اسکریپت پایتون را بارگذاری می‌کند و تابعی را که نام آن در argv[2] آمده است فراخوانی می‌کند. آرگومان‌های عدد صحیح آن، مقادیر دیگر آرایه‌ی argv هستند. اگر این برنامه را کامپایل و لینک کنید (فرض کنید نام پرونده اجرایی نهایی call باشد) و از آن برای اجرای یک اسکریپت پایتون استفاده کنید، مانند:

def multiply(a,b):
    print("Will compute", a, "times", b)
    c = 0
    for i in range(0, a):
        c = c + b
    return c

در این صورت نتیجه باید چنین باشد:

$ call multiply multiply 3 2
۳ ضربدر ۲ محاسبه خواهد شد
نتیجه‌ی فراخوانی: ۶

با اینکه این برنامه نسبت به کارکردش بسیار بزرگ است، بیشترِ کد آن برای تبدیل داده بین پایتون و C و برای گزارش خطا است. بخش جالب از نظر تعبیه پایتون با کد زیر آغاز می‌شود:

Py_Initialize();
pName = PyUnicode_DecodeFSDefault(argv[1]);
/* Error checking of pName left out */
pModule = PyImport_Import(pName);

پس از مقداردهی اولیه‌ی مفسر، اسکریپت با استفاده از PyImport_Import() بارگذاری می‌شود. این روتین به یک رشته پایتون به‌عنوان آرگومان خود نیاز دارد که با استفاده از روتین تبدیل داده‌ی PyUnicode_DecodeFSDefault() ساخته می‌شود.

pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc is a new reference */

if (pFunc && PyCallable_Check(pFunc)) {
    ...
}
Py_XDECREF(pFunc);

پس از بارگذاری اسکریپت، نامی که به دنبال آن هستیم با استفاده از PyObject_GetAttrString() بازیابی می‌شود. اگر نام وجود داشته باشد و شیء بازگردانده‌شده فراخوانی‌پذیر باشد، می‌توانید با اطمینان فرض کنید که آن یک تابع است. سپس برنامه به‌صورت معمول با ساخت تاپلی از آرگومان‌ها پیش می‌رود. فراخوانی تابع پایتون سپس به این صورت انجام می‌شود:

pValue = PyObject_CallObject(pFunc, pArgs);

پس از بازگشت تابع، pValue یا NULL است یا ارجاعی به مقدار بازگشتی تابع را در خود دارد. حتماً پس از بررسی مقدار، ارجاع را آزاد کنید.

1.4. توسعه پایتون تعبیه‌شده

تاکنون، مفسر تعبیه‌شده‌ی پایتون هیچ دسترسی‌ای به کارکردهای خودِ برنامه نداشت. API پایتون این کار را از طریق توسعه دادن مفسر تعبیه‌شده ممکن می‌سازد. یعنی مفسر تعبیه‌شده با روتین‌هایی که برنامه فراهم می‌کند، توسعه می‌یابد. اگرچه پیچیده به نظر می‌رسد، چندان هم بد نیست. فقط برای مدتی فراموش کنید که برنامه مفسر پایتون را راه‌اندازی می‌کند. در عوض، برنامه را مجموعه‌ای از زیرروتین‌ها در نظر بگیرید و کمی کد چسبان (glue code) بنویسید که دسترسی پایتون به آن روتین‌ها را فراهم کند، درست همان‌طور که یک توسعه‌ی معمولی پایتون می‌نویسید. برای مثال:

static int numargs=0;

/* Return the number of arguments of the application command line */
static PyObject*
emb_numargs(PyObject *self, PyObject *args)
{
    if(!PyArg_ParseTuple(args, ":numargs"))
        return NULL;
    return PyLong_FromLong(numargs);
}

static PyMethodDef emb_module_methods[] = {
    {"numargs", emb_numargs, METH_VARARGS,
     "Return the number of arguments received by the process."},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef emb_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "emb",
    .m_size = 0,
    .m_methods = emb_module_methods,
};

static PyObject*
PyInit_emb(void)
{
    return PyModuleDef_Init(&emb_module);
}

کد بالا را بلافاصله بالای تابع main() درج کنید. همچنین، دو دستور زیر را پیش از فراخوانی Py_Initialize() درج کنید:

numargs = argc;
PyImport_AppendInittab("emb", &PyInit_emb);

این دو سطر متغیر numargs را مقداردهی اولیه می‌کنند و تابع emb.numargs() را برای مفسر پایتون تعبیه‌شده در دسترس قرار می‌دهند. با این توسعه‌ها، اسکریپت پایتون می‌تواند کارهایی از این دست انجام دهد

import emb
print("Number of arguments", emb.numargs())

در یک برنامه‌ی واقعی، متدها API برنامه را برای پایتون در دسترس قرار خواهند داد.

1.5. تعبیه پایتون در C++

همچنین می‌توان پایتون را در یک برنامه C++ تعبیه کرد؛ اینکه این کار دقیقاً چگونه انجام می‌شود، به جزئیات سیستم C++ مورد استفاده بستگی دارد؛ به‌طور کلی باید برنامه اصلی را به C++ بنویسید و از کامپایلر C++ برای کامپایل و پیوند دادن برنامه خود استفاده کنید. نیازی نیست خود پایتون با C++ مجدداً کامپایل شود.

1.6. کامپایل و پیوند دادن در سیستم‌های شبه‌یونیکس

یافتن پرچم‌های مناسب برای دادن به کامپایلر (و پیونددهنده) شما به‌منظور تعبیه مفسر پایتون در برنامه‌تان، لزوماً کار ساده‌ای نیست؛ به‌ویژه از آن رو که پایتون باید ماژول‌های کتابخانه‌ای پیاده‌سازی‌شده به‌صورت توسعه‌های پویای C (پرونده‌های .so) که به آن پیوند شده‌اند را بارگذاری کند.

برای یافتن پرچم‌های مورد نیاز کامپایلر و پیونددهنده، می‌توانید اسکریپت pythonX.Y-config را اجرا کنید که به عنوان بخشی از فرایند نصب تولید می‌شود (ممکن است اسکریپت python3-config نیز در دسترس باشد). این اسکریپت چندین گزینه دارد که از میان آن‌ها، موارد زیر مستقیماً به کار شما خواهند آمد:

  • pythonX.Y-config --cflags پرچم‌های توصیه‌شده برای کامپایل را به شما می‌دهد:

    $ /opt/bin/python3.11-config --cflags
    -I/opt/include/python3.11 -I/opt/include/python3.11 -Wsign-compare  -DNDEBUG -g -fwrapv -O3 -Wall
    
  • pythonX.Y-config --ldflags --embed هنگام پیوند دادن، پرچم‌های پیشنهادی را به شما می‌دهد:

    $ /opt/bin/python3.11-config --ldflags --embed
    -L/opt/lib/python3.11/config-3.11-x86_64-linux-gnu -L/opt/lib -lpython3.11 -lpthread -ldl  -lutil -lm
    

توجه

برای جلوگیری از سردرگمی میان چندین نصب پایتون (و به‌ویژه میان پایتون سیستم و پایتون کامپایل‌شده‌ی خودتان)، توصیه می‌شود که از مسیر مطلق pythonX.Y-config استفاده کنید، همان‌طور که در مثال بالا آمده است.

اگر این روش برای شما کار نکند (تضمینی نیست که روی همه‌ی سکوهای شبه‌یونیکس کار کند؛ با این حال، از گزارش‌های اشکال استقبال می‌کنیم) ناچار خواهید بود مستندات سیستم خود را درباره‌ی پیونددهی پویا بخوانید و/یا Makefile پایتون (برای یافتن مکان آن از sysconfig.get_makefile_filename() استفاده کنید) و گزینه‌های کامپایل را بررسی کنید. در این صورت، ماژول sysconfig ابزاری مفید برای استخراج برنامه‌ای مقادیر پیکربندی‌ای است که می‌خواهید آن‌ها را با هم ترکیب کنید. برای مثال:

>>> import sysconfig
>>> sysconfig.get_config_var('LIBS')
'-lpthread -ldl  -lutil'
>>> sysconfig.get_config_var('LINKFORSHARED')
'-Xlinker -export-dynamic'