在其它 App 內嵌入 Python
************************

前面的章節討論了如何擴充 Python，也就是如何透過附加一個 C 函式庫來擴充
Python 的功能。但也可以反過來做：將 Python 嵌入你的 C/C++ 應用程式中。
嵌入讓你的應用程式能夠以 Python 而非 C 或 C++ 來實作應用程式的某些功能
。這可以用於許多目的；其中一個例子是允許使用者透過撰寫一些 Python 腳本
來根據他們的需求客製化應用程式。如果某些功能用 Python 寫起來比較容易，
你也可以自己使用這種方法。

嵌入 Python 與擴充 Python 類似，但不完全相同。差別在於當你擴充 Python
時，應用程式的主程式仍然是 Python 直譯器，而當你嵌入 Python，主程式可
能與 Python 無關 — 相反地，應用程式的某些部分偶爾會呼叫 Python 直譯器
來執行一些 Python 程式碼。

所以如果你要嵌入 Python，你要提供自己的主程式。這個主程式必須做的事情
之一是初始化 Python 直譯器，或至少必須要呼叫函式 "Py_Initialize()"。還
有一些可選的呼叫來傳遞命令列引數給 Python。然後你就可以在應用程式的任
何部分呼叫直譯器。

有幾種不同的方式來呼叫直譯器：你可以傳遞一個包含 Python 陳述式的字串給
"PyRun_SimpleString()"，或者你可以傳遞一個 stdio 檔案指標和檔案名稱（
僅用於錯誤訊息中的識別）給 "PyRun_SimpleFile()"。你也可以呼叫前面章節
中描述的較低層級操作來建構和使用 Python 物件。

也參考:

  Python/C API 參考手冊
     Python 的 C 介面詳細資訊在此手冊中提供。大量必要的資訊可以在這裡
     找到。


非常高階的嵌入
==============

嵌入 Python 最簡單的形式是使用非常高階的介面。此介面用於執行 Python 腳
本而無需直接與應用程式互動。例如這可以用來對檔案執行一些操作。

   #define PY_SSIZE_T_CLEAN
   #include <Python.h>

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

       /* 建議但非必要 */
       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 應該使用 "Py_ssize_t"
  而不是 "int"。從 Python 3.13 開始不再需要它，但我們保留它以維持向後
  相容性。關於此巨集的描述請參閱 字串與緩衝區。

"PyConfig.program_name" 的設定應該在 "Py_InitializeFromConfig()" 前呼
叫，以告知直譯器 Python run-time 函式庫的路徑。接下來，Python 直譯器會
用 "Py_Initialize()" 初始化，然後執行一個硬編碼的 Python 腳本來印出日
期和時間。之後，"Py_FinalizeEx()" 呼叫會關閉直譯器，接著程式結束。在真
實的程式中，你可能想要從另一個來源取得 Python 腳本，或許是文字編輯器例
程、檔案或資料庫。從檔案取得 Python 程式碼可以更好地使用
"PyRun_SimpleFile()" 函式來完成，這樣可以省去分配記憶體空間和載入檔案
內容的麻煩。


超越非常高階嵌入：概觀
======================

高階介面讓你能夠從應用程式中執行任意的 Python 程式碼片段，但交換資料值
的過程可以說相當繁瑣。如果你想進行這類操作，應該使用較低階的呼叫。雖然
需要撰寫更多的 C 程式碼，但幾乎可以實現任何功能。

需要注意的是，雖然目的不同，但擴充 Python 與嵌入 Python 其實是非常相似
的操作，前面章節討論的大多數主題在這裡同樣適用。為了說明這一點，請思考
從 Python 到 C 的擴充程式碼實際上做了什麼：

1. 將資料值從 Python 轉換為 C，

2. 使用轉換後的值呼叫 C 例程，並

3. 將呼叫中的資料值從 C 轉換為 Python。

當嵌入 Python 時，介面程式碼會：

1. 將資料值從 C 轉換為 Python，

2. 使用轉換後的值呼叫 Python 介面例程，並

3. 將呼叫中的資料值從 Python 轉換為 C。

如你所見，資料轉換的步驟只是互換了順序，以配合跨語言傳遞方向的不同。唯
一的差別在於兩個資料轉換之間所呼叫的例程：在擴充時你呼叫的是 C 例程；
在嵌入時則呼叫 Python 例程。

本章不會討論如何將資料從 Python 轉換為 C 或從 C 轉換回 Python，且假設
讀者已經知道參照的正確使用方式與錯誤處理。由於這些部分與擴充直譯器時相
同，相關資訊可參考前面的章節。


純嵌入
======

第一個程式的目標是執行 Python 腳本中的函式。就像在非常高階介面的章節中
一樣，Python 直譯器不會直接與應用程式互動（但這在下一節會改變）。

執行 Python 腳本中定義函式的程式碼是：

   #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]" 載入 Python 腳本，並呼叫 "argv[2]" 中所指定的函
式。其整數引數則來自 "argv" 陣列中的其他值。如果你編譯並連結此程式（我
們稱完成的可執行檔為 **call**），並用它來執行 Python 腳本，例如：

   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
   Will compute 3 times 2
   Result of call: 6

雖然以其功能而言這個程式相當龐大，但大部分的程式碼是用於 Python 與 C
之間的資料轉換以及錯誤回報。至於與嵌入 Python 有關的重點部分，則從以下
開始：

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

在初始化直譯器後，腳本使用 "PyImport_Import()" 載入。此例程需要一個
Python 字串作為其引數，該字串使用 "PyUnicode_DecodeFSDefault()" 資料轉
換例程建構。

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

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

腳本載入後，會使用 "PyObject_GetAttrString()" 來取得所需的名稱。如果該
名稱存在，且回傳的物件是可呼叫的，便可安全地假設它是一個函式。接著，程
式會以一般方式建立引數的元組。之後便以下列方式呼叫該 Python 函式：

   pValue = PyObject_CallObject(pFunc, pArgs);

函式回傳時，"pValue" 要不是 "NULL" 就是包含函式回傳值的參照。請務必在
檢查值之後釋放參照。


擴充嵌入式 Python
=================

到目前為止，嵌入式 Python 直譯器尚無法存取應用程式本身的功能。Python
API 允許透過擴充嵌入式直譯器來達成這點。也就是說，嵌入式直譯器可以由應
用程式所提供的例程加以擴充。雖然聽起來複雜但其實並不難。只要暫時忘記是
應用程式啟動了 Python 直譯器，改以將應用程式視為一組子程序，並撰寫一些
膠合程式碼讓 Python 能夠存取這些例程，就像你撰寫一般 Python 擴充一樣。
例如：

   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()" 函式可供嵌入式
Python 直譯器使用。有了這些擴充後，Python 腳本便可以執行像是以下的操作

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

在真實的應用程式中，這些方法會向 Python 公開應用程式的 API。


在 C++ 中嵌入 Python
====================

也可以將 Python 嵌入 C++ 程式中；具體如何做取決於所使用的 C++ 系統的細
節；一般來說，你需要用 C++ 撰寫主程式，並使用 C++ 編譯器來編譯和連結你
的程式。不需要使用 C++ 重新編譯 Python 本身。


在類 Unix 系統下編譯和連結
==========================

要找到傳遞給編譯器（和連結器）的正確旗標以便將 Python 直譯器嵌入你的應
用程式中，並不一定是簡單的事，特別是因為 Python 需要載入作為 C 動態擴
充（".so" 檔案）實作的函式庫模組，而這些模組又必須與 Python 進行連結。

要找出所需的編譯器和連結器旗標，你可以執行作為安裝過程的一部分產生的
"python*X.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

備註:

  為了避免多個 Python 安裝之間產生混淆（特別是系統自帶的 Python 與自行
  編譯的 Python 之間），建議使用 "python*X.Y*-config" 的絕對路徑，如上
  面的例子所示。

如果此程序對你不起作用（並不保證在所有類 Unix 平台上都能運作；不過我們
歡迎錯誤回報），你將需要參閱系統的動態連結相關文件，並／或檢查 Python
的 "Makefile"（可使用 "sysconfig.get_makefile_filename()" 來找到其位置
）和編譯選項。在這種情況下，"sysconfig" 模組是一個實用的工具，可用於以
程式化方式擷取並組合所需的組態值。例如：

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