【經驗分享】編譯Python靜態庫嵌入C/C++程序之一直踩坑一直爽

很多年沒寫文章,一是太忙,二是反正也沒多少人看(不過億)

既然三月是學雷鋒活動月,那就分享一點什麼吧~

人工智能莫名其妙的把Python給點燃了,於是一個軟件計劃用Python腳本作爲配置

這樣甚至很多中學生都可以寫配置腳本來設置軟件相關參數了

Python本身是C/C++編寫的,至於如何編譯Python爲靜態庫,這裏不再贅述(如要求強烈我再另文詳解)

清單:

Python 2.7.8
Microsoft Visual Studio 2015
Windows 10

當你興致勃勃鏈接到C/C++程序,編譯得到2.56M的程序,編寫腳本執行時發現初始化閃退~

單步跟蹤:發現Py_InitializeEx()函數執行到此處

    if (!Py_NoSiteFlag)
        initsite(); /* Module site */

因爲嵌入不需要第三方包,在初始化前加一行屏蔽即可:

Py_NoSiteFlag = 1;
Py_Initialize();
if (Py_IsInitialized() != 0)
{
    // ...
}

當你導出函數,模塊給Python,然後再Python腳本中調用,會發現PyImport_Import()總是返回空(NULL),丫的

那麼總要知道具體原因吧,寫幾行代碼:

PyObject *type = NULL;
PyObject *value = NULL;
PyObject *traceback = NULL;
// 
PyErr_Fetch(&type, &value, &traceback);
if (type != nullptr)
{
	printf(PyExceptionClass_Name(type));
}
if (value != nullptr)
{
	// PyObject_Repr()
	// Return value: New reference.
	PyObject *line = PyObject_Str(value);
	if (line != nullptr)
	{
		printf(PyString_AsString(line));
		// Py_DecRef();
		Py_DECREF(line);
	}
}
// We do not need Strack Trace back here.

那麼報錯出來了:Import by filename is not supported.

也就是說,你不能使用全路徑來載入Python腳本(只能是純文件名,可以帶擴展名)

那就改唄,用純文件名testpy.py導入,這樣你滿意了吧!

結果:no module named testpy.py

最直接的反應,這貨沒有到指定的目錄裏取搜索腳本文件,於是有下面一堆複雜的分析和資料查閱

有些資料你必須跑到國外去查,百度是不可能查到的:

1.默認的Python.exe會使用PYTHONHOME和PYTHONPATH兩個環境變量

2.PYTHONHOME是Python的安裝目錄,而PYTHONPATH是一個列表一般是PYTHONHOME的子目錄

3.可以在Py_Initialize()之前調用Py_SetPythonHome指定PYTHONHOME

然而PYTHONHOME跟模塊搜索目錄狗屁關係沒有(其實還是有的),Python搜索模塊的目錄可以通過Py_GetPath()獲取

獲取到發現是:

應用程序名稱.zip;.\\DLLs;.\\lib;.\\lib\\plat-win;.\\lib\\lib-tk;當前目錄

當前目錄是可以隨意改的,反過來也不能隨意改,比如你寫的是一個系統服務程序,怎麼好亂改~

既然有Py_GetPath()就應該有Py_SetPath()纔對,沒錯,不過它是Python 3的接口!(告你版本歧視)

跟蹤Py_Initialize()發現初始化過程有這麼幾行:

sysmod = _PySys_Init();
//……
PySys_SetPath(Py_GetPath());

難道這就是“未公開的”接口嗎,再次狂翻資料(3x0不斷提示:磁盤要整理了,桌面要整理了),然後:

void PySys_SetPath(char *path)

    Set sys.path to a list object of paths found in path which should be a list of paths separated with the platform’s search path delimiter (: on Unix, ; on Windows).

薩滿巫醫:It's a cook book! A cook book!

這貨設定的路徑是給腳本用的,也就是隻影響腳本里的sys.path那一堆(what ever it is)

反正寫了很多代碼,驗證確實是這樣,我的指甲喲~

跟蹤Py_GetPath()發現

char *pythonhome = Py_GetPythonHome();
char *envpath = Py_GETENV("PYTHONPATH");
//……
bufsz += strlen(PYTHONPATH) + 1;
bufsz += strlen(argv0_path) + 1;

也即是說,本身有PYTHONPATH這個宏

#ifndef PYTHONPATH
#define PYTHONPATH ".\\DLLs;.\\lib;.\\lib\\plat-win;.\\lib\\lib-tk"
#endif

改掉它是可以解決問題的,可是宏有一個問題,如果C/C++代碼是另一部分人寫的

或者由於各種原因不能更改後重新編譯,這個方法就雞肋了,看來更改進程的環境變量纔是正道

// Changes via SetEnvironmentVariable() do not take effect in library that uses getenv()
// It's not going to work!
char c;
DWORD bytes = GetEnvironmentVariableA("PYTHONPATH", &c, 1);
if (bytes > 0)
{
	char *szEnv = nullptr;
	szEnv = (char *)malloc(bytes + 1 + strlen(pathName));
	if (szEnv != nullptr)
	{
		bytes = GetEnvironmentVariableA("PYTHONPATH", szEnv, bytes);
		szEnv[bytes - 1] = ';';
		strcpy(szEnv + bytes, pathName);
		//OutputDebugStringA(szEnv);
		if (SetEnvironmentVariableA("PYTHONPATH", szEnv) == false)
		{
			OutputDebugStringA("SetEnvironmentVariableA(PYTHONPATH) falied.\r\n");
		}
		free(szEnv);
	}
}
else
{
	if (SetEnvironmentVariableA("PYTHONPATH", pathName) == false)
	{
		OutputDebugStringA("SetEnvironmentVariableA(PYTHONPATH) falied.\r\n");
	}
}

運行發現:無用也!袁崇煥,你滴良心大大滴壞啦!

又開始翻找資料:還是國外才有的查,不過微軟停更Windows7後很多資料只有第三方纔有存留

getenv makes a copy of the environment variable block of the process on startup.
Any subsequent changes via SetEnvironmentVariable will not be reflected in the block of variables used by getenv.
You will need to pinvoke the setenv function to have the adjusted the value reflected in subsequent getenv calls.

See: http://msdn.microsoft.com/en-us/library/tehxacec(VS.71).aspx

getenv and _putenv use the copy of the environment pointed to by the global variable _environ to access the environment.
getenv operates only on the data structures accessible to the run-time library and not on the environment "segment" created for the process by the operating system.
Therefore, programs that use the envp argument to main or wmain may retrieve invalid information.

太黑暗了,這什麼世道啊~

仔細觀察發現Py_GETENV()這個宏,發現:

/* this is a wrapper around getenv() that pays attention to
   Py_IgnoreEnvironmentFlag.  It should be used for getting variables like
   PYTHONPATH and PYTHONHOME from the environment */
#define Py_GETENV(s) (Py_IgnoreEnvironmentFlag ? NULL : getenv(s))

因爲

getenv & setenv require #include <stdlib.h> BUT for Linux ONLY

所以必須自己寫這個東西:

// It's going to work!
char *oldEnv = getenv("PYTHONPATH");
if (oldEnv != nullptr && oldEnv[0] != '\0')
{
	//char *szEnv = (char *)malloc(sizeof("PYTHONPATH") + strlen(oldEnv) + 1 + strlen(pathName) + 1);
	sprintf(Script::SysPath, "PYTHONPATH=%s;%s", pathName, oldEnv);
}
else
{
	sprintf(Script::SysPath, "PYTHONPATH=%s", pathName);
}
if (putenv(Script::SysPath) != 0)
{
	OutputDebugStringA("putenv(PYTHONPATH) falied.\r\n");
}

這裏我直接用靜態緩衝區代替動態分配內存,在Py_Initialize()之前調用,問題解決

Python腳本一般不會有什麼錯,這種腳本語法據說很“優雅”(I do not care)

好了,問題解決!編寫接口文檔,把底層開發的痛苦留給自己,腳本編寫的快樂留給使用者吧(錢還是要付的)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章