很多年没写文章,一是太忙,二是反正也没多少人看(不过亿)

既然三月是学雷锋活动月,那就分享一点什么吧~

人工智能莫名其妙的把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)

好了,问题解决!编写接口文档,把底层开发的痛苦留给自己,脚本编写的快乐留给使用者吧(钱还是要付的)