1. Nhúng Python vào ứng dụng khác¶
Các chương trước đã thảo luận về cách mở rộng Python, nghĩa là cách mở rộng chức năng của Python bằng cách đính kèm một thư viện các hàm C vào nó. Cũng có thể làm theo cách khác: làm phong phú ứng dụng C/C++ của bạn bằng cách nhúng Python vào đó. Nhúng cung cấp cho ứng dụng của bạn khả năng triển khai một số chức năng của ứng dụng bằng Python thay vì C hoặc C++. Điều này có thể được sử dụng cho nhiều mục đích; một ví dụ là cho phép người dùng điều chỉnh ứng dụng theo nhu cầu của họ bằng cách viết một số tập lệnh bằng Python. Bạn cũng có thể tự mình sử dụng nó nếu một số chức năng có thể được viết bằng Python dễ dàng hơn.
Nhúng Python cũng tương tự như mở rộng nó, nhưng không hoàn toàn như vậy. Điểm khác biệt là khi bạn mở rộng Python, chương trình chính của ứng dụng vẫn là trình thông dịch Python, trong khi nếu bạn nhúng Python, chương trình chính có thể không liên quan gì đến Python --- thay vào đó, một số phần của ứng dụng thỉnh thoảng gọi trình thông dịch Python để chạy một số mã Python.
Vì vậy, nếu bạn nhúng Python, bạn đang cung cấp chương trình chính của riêng mình. Một trong những điều mà chương trình chính này phải làm là khởi tạo trình thông dịch Python. Ít nhất bạn phải gọi hàm Py_Initialize(). Có các lệnh gọi tùy chọn để truyền đối số dòng lệnh cho Python. Sau đó, bạn có thể gọi trình thông dịch từ bất kỳ phần nào của ứng dụng.
Có một số cách khác nhau để gọi trình thông dịch: bạn có thể chuyển một chuỗi chứa các câu lệnh Python tới PyRun_SimpleString() hoặc bạn có thể chuyển một con trỏ tệp stdio và tên tệp (chỉ để nhận dạng trong các thông báo lỗi) tới PyRun_SimpleFile(). Bạn cũng có thể gọi các thao tác cấp thấp hơn được mô tả trong các chương trước để xây dựng và sử dụng các đối tượng Python.
Xem thêm
- Hướng dẫn tham khảo Python/C API
Chi tiết về giao diện C của Python được đưa ra trong hướng dẫn này. Rất nhiều thông tin cần thiết có thể được tìm thấy ở đây.
1.1. Nhúng cấp độ rất cao¶
Hình thức nhúng Python đơn giản nhất là sử dụng giao diện cấp rất cao. Giao diện này nhằm mục đích thực thi tập lệnh Python mà không cần tương tác trực tiếp với ứng dụng. Ví dụ, điều này có thể được sử dụng để thực hiện một số thao tác trên một tệp.
#define PY_SSIZE_T_CLEAN
#include <Python.h>
int
chính(int argc, char *argv[])
{
Trạng thái PyStatus;
Cấu hình PyConfig;
PyConfig_InitPythonConfig(&config);
/* tùy chọn nhưng được khuyến nghị */
status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
if (PyStatus_Exception(trạng thái)) {
ngoại lệ goto;
}
trạng thái = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(trạng thái)) {
ngoại lệ goto;
}
PyConfig_Clear(&config);
PyRun_SimpleString("từ thời điểm nhập thời gian,ctime\n"
"print('Hôm nay là', ctime(time()))\n");
nếu (Py_FinalizeEx() < 0) {
thoát (120);
}
trả về 0;
ngoại lệ:
PyConfig_Clear(&config);
Py_ExitStatusException(trạng thái);
}
Ghi chú
#define PY_SSIZE_T_CLEAN được dùng để chỉ ra rằng nên sử dụng Py_ssize_t trong một số API thay vì int. Nó không cần thiết kể từ Python 3.13, nhưng chúng tôi giữ nó ở đây để tương thích ngược. Xem Chuỗi và bộ đệm để biết mô tả về macro này.
Cài đặt PyConfig.program_name phải được gọi trước Py_InitializeFromConfig() để thông báo cho trình thông dịch về đường dẫn đến thư viện thời gian chạy Python. Tiếp theo, trình thông dịch Python được khởi tạo bằng Py_Initialize(), sau đó là thực thi tập lệnh Python được mã hóa cứng để in ngày và giờ. Sau đó, lệnh gọi Py_FinalizeEx() sẽ tắt trình thông dịch, sau đó là kết thúc chương trình. Trong một chương trình thực, bạn có thể muốn lấy tập lệnh Python từ một nguồn khác, có thể là quy trình soạn thảo văn bản, tệp hoặc cơ sở dữ liệu. Việc lấy mã Python từ một tệp có thể được thực hiện tốt hơn bằng cách sử dụng hàm PyRun_SimpleFile(), giúp bạn tránh khỏi rắc rối khi phân bổ không gian bộ nhớ và tải nội dung tệp.
1.2. Ngoài khả năng nhúng cấp độ rất cao: Tổng quan¶
Giao diện cấp cao cung cấp cho bạn khả năng thực thi các đoạn mã Python tùy ý từ ứng dụng của bạn, nhưng ít nhất việc trao đổi các giá trị dữ liệu khá phức tạp. Nếu muốn điều đó, bạn nên sử dụng các cuộc gọi cấp thấp hơn. Với cái giá phải viết thêm mã C, bạn có thể đạt được hầu hết mọi thứ.
Cần lưu ý rằng việc mở rộng Python và nhúng Python là hoạt động khá giống nhau, mặc dù mục đích khác nhau. Hầu hết các chủ đề được thảo luận trong các chương trước vẫn còn giá trị. Để hiển thị điều này, hãy xem xét mã mở rộng từ Python sang C thực sự làm gì:
Chuyển đổi giá trị dữ liệu từ Python sang C,
Thực hiện lệnh gọi hàm tới quy trình C bằng cách sử dụng các giá trị được chuyển đổi và
Chuyển đổi các giá trị dữ liệu từ cuộc gọi từ C sang Python.
Khi nhúng Python, mã giao diện sẽ:
Chuyển đổi giá trị dữ liệu từ C sang Python,
Thực hiện lệnh gọi hàm tới quy trình giao diện Python bằng cách sử dụng các giá trị được chuyển đổi và
Chuyển đổi các giá trị dữ liệu từ cuộc gọi từ Python sang C.
Như bạn có thể thấy, các bước chuyển đổi dữ liệu được hoán đổi đơn giản để phù hợp với hướng chuyển giao ngôn ngữ chéo khác nhau. Sự khác biệt duy nhất là quy trình mà bạn gọi giữa cả hai lần chuyển đổi dữ liệu. Khi mở rộng, bạn gọi thủ tục C, khi nhúng, bạn gọi thủ tục Python.
Chương này sẽ không thảo luận về cách chuyển đổi dữ liệu từ Python sang C và ngược lại. Ngoài ra, việc sử dụng hợp lý các tài liệu tham khảo và xử lý các lỗi được cho là được hiểu rõ. Vì những khía cạnh này không khác với việc mở rộng trình thông dịch nên bạn có thể tham khảo các chương trước để biết thông tin cần thiết.
1.3. Nhúng thuần túy¶
Chương trình đầu tiên nhằm mục đích thực thi một hàm trong tập lệnh Python. Giống như trong phần về giao diện cấp rất cao, trình thông dịch Python không tương tác trực tiếp với ứng dụng (nhưng điều đó sẽ thay đổi trong phần tiếp theo).
Mã để chạy một hàm được xác định trong tập lệnh Python là:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
int
chính(int argc, char *argv[])
{
PyObject *pName, *pModule, *pFunc;
PyObject *pArgs, *pValue;
int tôi;
nếu (argc < 3) {
fprintf(stderr,"Cách sử dụng: gọi pythonfile funcname [args]\n");
trả về 1;
}
Py_Initialize();
pName = PyUnicode_DecodeFSDefault(argv[1]);
/* Lỗi kiểm tra pName bị bỏ sót */
pModule = PyImport_Import(pName);
Py_DECREF(pName);
if (pModule != NULL) {
pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc là một tham chiếu mới */
if (pFunc && PyCallable_Check(pFunc)) {
pArgs = PyTuple_New(argc - 3);
cho (i = 0; i < argc - 3; ++i) {
pValue = PyLong_FromLong(atoi(argv[i + 3]));
nếu (!pValue) {
Py_DECREF(pArss);
Py_DECREF(pModule);
fprintf(stderr, "Không thể chuyển đổi đối số\n");
trả về 1;
}
/* Tham chiếu pValue bị đánh cắp ở đây: */
PyTuple_SetItem(pArgs, i, pValue);
}
pValue = PyObject_CallObject(pFunc, pArgs);
Py_DECREF(pArss);
nếu (pValue != NULL) {
printf("Kết quả cuộc gọi: %ld\n", PyLong_AsLong(pValue));
Py_DECREF(pValue);
}
khác {
Py_DECREF(pFunc);
Py_DECREF(pModule);
PyErr_Print();
fprintf(stderr,"Gọi thất bại\n");
trả về 1;
}
}
khác {
nếu (PyErr_Occurred())
PyErr_Print();
fprintf(stderr, "Không tìm thấy hàm \"%s\"\n", argv[2]);
}
Py_XDECREF(pFunc);
Py_DECREF(pModule);
}
khác {
PyErr_Print();
fprintf(stderr, "Không tải được \"%s\"\n", argv[1]);
trả về 1;
}
nếu (Py_FinalizeEx() < 0) {
trả lại 120;
}
trả về 0;
}
Mã này tải tập lệnh Python bằng argv[1] và gọi hàm có tên trong argv[2]. Các đối số nguyên của nó là các giá trị khác của mảng argv. Nếu bạn compile and link chương trình này (hãy gọi call thực thi đã hoàn thành) và sử dụng nó để thực thi tập lệnh Python, chẳng hạn như:
def nhân(a,b):
print("Sẽ tính toán", a, "lần", b)
c = 0
cho i trong phạm vi (0, a):
c = c + b
trả lại c
thì kết quả sẽ là:
$ gọi nhân nhân 3 2
Sẽ tính 3 lần 2
Kết quả cuộc gọi: 6
Mặc dù chương trình này khá lớn về chức năng nhưng hầu hết mã là để chuyển đổi dữ liệu giữa Python và C cũng như để báo cáo lỗi. Phần thú vị liên quan đến việc nhúng Python bắt đầu bằng
Py_Initialize();
pName = PyUnicode_DecodeFSDefault(argv[1]);
/* Lỗi kiểm tra pName bị bỏ sót */
pModule = PyImport_Import(pName);
Sau khi khởi tạo trình thông dịch, tập lệnh sẽ được tải bằng PyImport_Import(). Quy trình này cần một chuỗi Python làm đối số, được xây dựng bằng quy trình chuyển đổi dữ liệu PyUnicode_DecodeFSDefault().
pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc là một tham chiếu mới */
if (pFunc && PyCallable_Check(pFunc)) {
...
}
Py_XDECREF(pFunc);
Sau khi tập lệnh được tải, tên mà chúng tôi đang tìm kiếm sẽ được truy xuất bằng PyObject_GetAttrString(). Nếu tên tồn tại và đối tượng trả về có thể gọi được, bạn có thể giả định rằng đó là một hàm một cách an toàn. Sau đó, chương trình tiến hành xây dựng một bộ đối số như bình thường. Lệnh gọi hàm Python sau đó được thực hiện với:
pValue = PyObject_CallObject(pFunc, pArgs);
Khi trả về hàm, pValue là NULL hoặc chứa tham chiếu đến giá trị trả về của hàm. Hãy chắc chắn giải phóng tham chiếu sau khi kiểm tra giá trị.
1.4. Mở rộng Python nhúng¶
Cho đến nay, trình thông dịch Python nhúng không có quyền truy cập vào chức năng từ chính ứng dụng. Python API cho phép điều này bằng cách mở rộng trình thông dịch nhúng. Nghĩa là, trình thông dịch nhúng được mở rộng với các quy trình do ứng dụng cung cấp. Mặc dù nghe có vẻ phức tạp nhưng nó không quá tệ. Đơn giản chỉ cần quên mất một lúc rằng ứng dụng sẽ khởi động trình thông dịch Python. Thay vào đó, hãy coi ứng dụng là một tập hợp các chương trình con và viết một số mã keo cho phép Python truy cập vào các chương trình đó, giống như bạn viết một phần mở rộng Python thông thường. Ví dụ:
số int tĩnh = 0;
/* Trả về số đối số của dòng lệnh ứng dụng */
PyObject tĩnh*
emb_numargs(PyObject *self, PyObject *args)
{
if(!PyArg_ParseTuple(args, ":numargs"))
trả lại NULL;
trả về PyLong_FromLong(numargs);
}
PyMethodDef tĩnh emb_module_methods[] = {
{"numargs", emb_numargs, METH_VARARGS,
"Trả về số lượng đối số mà tiến trình nhận được."},
{NULL, NULL, 0, NULL}
};
cấu trúc tĩnh PyModuleDef emb_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "emb",
.m_size = 0,
.m_methods = emb_module_methods,
};
PyObject tĩnh*
PyInit_emb(void)
{
trả về PyModuleDef_Init(&emb_module);
}
Chèn đoạn mã trên ngay phía trên hàm main(). Ngoài ra, hãy chèn hai câu lệnh sau trước lệnh gọi tới Py_Initialize():
chữ số = argc;
PyImport_AppendInittab("emb", &PyInit_emb);
Hai dòng này khởi tạo biến numargs và làm cho trình thông dịch Python nhúng có thể truy cập được hàm emb.numargs(). Với những tiện ích mở rộng này, tập lệnh Python có thể thực hiện những việc như
nhập khẩu
print("Số đối số", emb.numargs())
Trong một ứng dụng thực tế, các phương thức sẽ hiển thị API của ứng dụng cho Python.
1.5. Nhúng Python vào C++¶
Cũng có thể nhúng Python vào chương trình C++; chính xác việc này được thực hiện như thế nào sẽ phụ thuộc vào chi tiết của hệ thống C++ được sử dụng; nói chung, bạn sẽ cần phải viết chương trình chính bằng C++ và sử dụng trình biên dịch C++ để biên dịch và liên kết chương trình của mình. Không cần phải biên dịch lại Python bằng C++.
1.6. Biên dịch và liên kết trong các hệ thống giống Unix¶
Việc tìm đúng cờ để chuyển đến trình biên dịch (và trình liên kết) của bạn để nhúng trình thông dịch Python vào ứng dụng của bạn không hẳn là chuyện đơn giản, đặc biệt vì Python cần tải các mô-đun thư viện được triển khai dưới dạng phần mở rộng động C (tệp .so) được liên kết với nó.
Để tìm ra các cờ trình biên dịch và trình liên kết cần thiết, bạn có thể thực thi tập lệnh pythonX.Y-config được tạo như một phần của quá trình cài đặt (tập lệnh python3-config cũng có thể có sẵn). Tập lệnh này có một số tùy chọn, trong đó những tùy chọn sau sẽ hữu ích trực tiếp cho bạn:
pythonX.Y-config --cflagssẽ cung cấp cho bạn các cờ được đề xuất khi biên dịch:$ /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 --embedsẽ cung cấp cho bạn các cờ được đề xuất khi liên kết:$ /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
Ghi chú
Để tránh nhầm lẫn giữa một số cài đặt Python (và đặc biệt là giữa Python hệ thống và Python được biên dịch của riêng bạn), bạn nên sử dụng đường dẫn tuyệt đối đến pythonX.Y-config, như trong ví dụ trên.
Nếu quy trình này không hiệu quả với bạn (nó không đảm bảo sẽ hoạt động trên tất cả các nền tảng giống Unix; tuy nhiên, chúng tôi hoan nghênh bug reports), bạn sẽ phải đọc tài liệu hệ thống của mình về liên kết động và/hoặc kiểm tra Makefile của Python (sử dụng sysconfig.get_makefile_filename() để tìm vị trí của nó) và các tùy chọn biên dịch. Trong trường hợp này, mô-đun sysconfig là một công cụ hữu ích để trích xuất các giá trị cấu hình mà bạn muốn kết hợp với nhau theo chương trình. Ví dụ:
>>> nhập cấu hình hệ thống
>>> sysconfig.get_config_var('LIBS')
'-lpthread -ldl -lutil'
>>> sysconfig.get_config_var('LINKFORSHARED')
'-Xlinker -export-động'