脚本宝典收集整理的这篇文章主要介绍了Python与Javascript相互调用超详细讲解(2022年1月最新)(三)基本原理Part 3 - 通过C/C++联通,脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。
首先要明白的是,javascript和python都是解释型语言,它们的运行是需要具体的runtime的。
collections
这种)都用不了。在本文叙述中,假定:
- 主语言: 最终的主程序所用的语言
- 副语言: 不是主语言的另一种语言
例如,python调用js,python就是主语言,js是副语言
个人觉得这是最佳方案了,但是老一点的技术文章几乎没有人说。也可能是前几年javascript的C语言相关的技术还没发展得很全面?
只要选好合适的库,几乎适用于绝大部分场景:
有库!有库!有库!
pip install -v pyv8
pyv8
升级,把Python API改成了python3的。
gyp-next
:pip install gyp-next
,然后npm install @fridgerator/pynode
。
boa
相比,非常轻量,只有几十MB,boa
有几百MB。startInterpreter()
起2次python解释器的话会有segmentation fault(可以解决!)npm install @pipcook/boa
。阿里的开源库,跟pynode基本也是同一个原理。如果没有缺点里所说的情况,更推荐用这个。
with
语句之类的。boa
只能使用conda的python runtime,那么结果就是python部分的依赖的包要么同时在这两个runtime里都装一遍,要么只在一个里装,把site-packages
添加到另一个runtime的sys.path
下。如果包装在本地runtime里,那么conda的runtime可能要把本地runtime的库目录,以及本地虚拟环境的库目录都加进入,我觉得挺麻烦的。numpy
之类的。感觉非常适合浏览器使用python进行科学计算!众所周知(不是x
事就这么成了,让这两种语言通过C语言来联通!这种方式的好处是:
我根据自己见过的项目,总结了一下目前已有的方式:
参考:《NodeJS and Python interoperability!》(题外话:才发现就是PyNode项目的作者写的!TQL!)
这篇文章把这条路的基本原理写得很清楚了,我稍微翻译一下大意,尽量达,不信不雅(x)。
NodeJS与Python之间的互操作还是相对简单 可行的。我不是指基于子进程调用CLI以及进程间通信的方式,或者一些其它古怪的方式。这两种语言都是用C/C++编写的,所以通过调用副语言的本机库,互操作是可能的。看看我使用这两种语言的底层API的过程中都经历了什么吧,我可真的很不喜欢这个!🙁🤣(就是这么......直白)。
那么,本着本博客的精神,啤酒⇓[Drumroll APA.jpg]
Node是基于V8编写的。你可以在C++中创建javascript类和函数,并在javascript数据类型和V8数据类型之间轻松转换,来传递参数和返回值。我发现这非常有利于在C++中进行数据处理——javascript(相比C)实在太慢了。然后,我可以将(处理好的)大型数组返回给javascript来绘制图形,并且不需要先对这个大数组进行序列化/反序列化。使用N-API的话,这种数据类型转换甚至变得更加容易。我不打算讨论使用V8和N-API的具体细节,但是有很多关于这个主题的博文,以及大量的基于此原理的Node.js模块可以作为样例学习。
译注:1)Node-API,或N-API:Node.js更高级一层的应用程序二进制接口 (ABI),对开发者隐藏了底层引擎(指V8)。2)大意就是:转换C和javascript数据类型很方便,不需要像进程间通信(IPC)那像做复杂的字符串解析。
在Python里也有个差不多的概念——嵌入Python (Embedding Python)。你可以用它运行Python代码片段,或打开已有的文件(模块)并直接调用函数。同样,它也需要在C++和python的数据类型间进行转换,来传递参数和返回值。此外,要做到这个你还需要一个Python解释器(但依然可以让你的项目保有可移植性,我会在后面详细讲讲这个)。awasu.com上有一篇非常好的博文,关于如何用C++编写Python包装器给出了非常详细的解释和例子。
译注:大意就是,参考了一个博文,可以做到基于python的C-API,给Python对象写一个包装器,把那些类型转换都处理好,使得C代码调用python对象就像在调用C对象一样。
完整的代码在这里
首先,在Initialize
函数里,我们设置了一些搜索路径,使得Python能够找到解释器和所需的库。然后我们将它们传递给Py_SetPath
。接下来,我们初始化Python解释器,并把当前目录加入python的sys.path
里,这样这个解释器就可以找到这个目录里的Python模块。最后,我们让Python解释器解码我们的tools.py文件,并把它作为模块导入,这样后面就可以调用它。
void Initialize(v8::Local<v8::Object> exports)
{
// Initialize Python
std::wstring path(L"/usr/lib/python3.7:/usr/local/lib/python3.7/lib-dynload:/usr/local/lib/python3.7/site-packages");
const wchar_t *stdlib = path.c_str();
Py_SetPath(stdlib);
exports->Set(
Nan::New("multiply").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(Multiply)->GetFunction());
Py_Initialize();
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append(".")");
PyObject *pName;
pName = PyUnicode_DecodeFSDefault("tools");
pModule = PyImport_Import(pName);
Py_DECREF(pName);
if (pModule == NULL)
{
PyErr_Print();
fprintf(stderr, "Failed to load %s"n", "tools");
Nan::ThrowError("Failed to load python module");
return;
}
}
我们已经在我们的Node模块的exports
里添加了一个函数multiply
,它会调用我们C++里的Multiply
函数。
译注:1)这个函数已经通过
NODE_MODULE(addon, Initialize);
被我们指定为Node.js模块的入口,所以这个函数的参数才是exports
,往它里面添加东西就相当于执行module.exports = {...}
2)exports->Set
的那个语句相当于在javscript里写:
module.exports = { multiply: Multiply }
void Multiply(const Nan::FunctionCallbackInfo<v8::Value> &args)
{
if (!args[0]->IsNumber() || !args[1]->IsNumber())
{
Nan::ThrowError("Arguments must be a number");
return;
}
PyObject *pFunc, *pArgs, *pValue;
double a = Nan::To<double>(args[0]).FromJust();
double b = Nan::To<double>(args[1]).FromJust();
pFunc = PyObject_GetAttrString(pModule, "multiply");
if (pFunc && PyCallable_Check(pFunc))
{
pArgs = PyTuple_New(2);
PyTuple_SetItem(pArgs, 0, PyLong_FromLong(a));
PyTuple_SetItem(pArgs, 1, PyLong_FromLong(b));
pValue = PyObject_CallObject(pFunc, pArgs);
Py_DECREF(pArgs);
if (pValue != NULL)
{
long result = PyLong_AsLong(pValue);
Py_DECREF(pValue);
args.GetReturnValue().Set(Nan::New((double)result));
}
else
{
Py_DECREF(pFunc);
PyErr_Print();
fprintf(stderr, "Call failedn");
Nan::ThrowError("Function call failed");
}
}
}
(在Multiply
函数里),我们在检查了参数后,使用方便的Nan::To
把传来的(基于Node数据类型的)参数转换成(C++)的double类型。然后,我们使用PyObject_GetAttrString
加载Python函数,并使用PyCallable_Check
确保我们已经找到了一个可调用的函数。
假设我们有两个从javascript传来的有效参数,并且我们已经(在python里)找到了一个可调用的multiply
函数,下一步就是把这两个double变量转换成Python函数的参数。我们创建一个大小为2的新的Python元组(tuple),并把这两个double变量添加到该元组中。现在是见证奇迹的时刻:pValue = PyObject_CallObject(pFunc, pArgs);
。如果pValue
不是NULL
,那我们就已经成功地在Node里调用了Python函数且得到了返回值。最后,我们把pValue
转换为一个长整型long
,并把它设置成我们的Node函数的返回值。
我觉得,这真特么的酷毙了。
译注:这个Demo可能做得比较早,现在Node.js这一系列API已经改名叫Node-API/N-API了,在C++里的namespace也变了。N-API的设计可能也有所区别,但是联通python和javascript的原理还是一样的,体会精神hhh
在这个Demo里,我在本地下载并构建(build)了Python 3.7.3。如果你查看binding.gyp
文件,你会注意到它包括了(Python的)本地文件夹。你也可以建立一个可移植的Python发行版,与Node App一起分发。这对Electron App来说可能很有用。另一篇博文描述了如何在OSX中这样做。
译注:1)
binding.gyp
里连到了他本地构建的python的头文件,注释掉的部分是连接本地编译好的动态库的,现在没注释掉的部分连的是本机python的动态库。2)没太Get到,可能大意是编译Node.js可执行文件的时候直接把python一起编译进去?不过现在pynode不是这么做的,回头再写篇实践经验分享吧。
这当然比使用child_process.spawn
来运行python要费劲得多。我也不确定这些额外的工作值不值得(译注:超值得!)。它是(这两种语言间)更直接的调用(相比与进程间通信),并且好处是只需要转换参数和返回值。此外,我们甚至可以把Python文件通过xxd -i tools.py
进行hexdump(转成十六进制数据),然后作为一个C的char变量,在编译的时候把它包括进去。我会继续基于这个想法做更多深度,探索出更多的可能性(译注:然后就有了pynode!感恩!)。
最后,补充一下,这种方案说是python和javascript的互操作,但最后运行App的主语言需要是javascript,并且需要找到python的动态/静态库来build你的Node.js包,之后才能利用这个包随意调用python代码。我的感受是,我们装python的时候,大概率会装好python的动态库(大概是因为用Python的C API写python包的需求实在很常见),找到它也比较容易。
事件起源于Google有个古老的python项目pyv8,大概的功能就是可以通过python API来调用V8引擎,执行javascript的代码。原理跟前面所述的也差不多,都是利用python和V8各自的C API来联通,进行一些数据类型转换。只不过区别是,上面是javascript调用python,所以最后需要python方的动态或静态库,这边是python调用javascript,所以最后需要javascript方(也就是V8)的动态/静态库。
但这种方式有两个问题:
一是,我们装Node的时候,大概率装好了头文件和二进制可执行文件,可能没带共享库,想要库文件只能通过源码编译。而V8,安装它甚至需要从源码构建(……)。总之,通过嵌入V8来执行javascript会导致你的python app安装起来非常累。找Javascript runtime的动态/静态库就像是,你每到一个地方,都要现场招聘一个可心的翻译。而找Python的就像是,只要我集团有业务的地方,都安排好翻译了,你只要把他叫来就行(……破烂比喻)。
二是,嵌入的不是Node.js而是V8的话,有很多Node.js特有的特性你用不了。比如require
包之类的,如果你的JS代码本来就是基于Node.js开发的话,这就会很痛苦。你需要把你的基于Node的JS代码转换成V8可以懂的单文件JS代码(倒是有包可以做到,见这里)。
那么可不可以嵌入Node.js呢?倒是也可以,Node有一篇简单的文档,需要做的大概就是把上一种方法反过来。但目前没人这么干过,不如说,别说是python嵌Node.js了,C++嵌Node.js的项目也不多,遇到问题可能很难解决。而且Node.js安装的时候不带共享库,只能从源码构建,也挺烦人的。如果想简单嵌入一些node.js的功能,可能libnode也能一定程度上解决?
这种方法的底层逻辑(x)虽然跟C语言有关,但和前面两种完全不同。我也没实践过,只把自己查资料得到的一些粗浅理解列出来了。
参考:https://github.com/chenshenhai/blog/issues/38如何看待 WebAssembly 这门技术?
首先,我们要知道,虽然python和javascript之类的底层引擎是用C/C++语言写的,但归根到底C/C++也是一种高级语言。它也需要被编译成汇编的机器指令,并链接相应的库之后生成二进制可执行文件来执行。
万恶之源 起因是WebAssembly,它是一种汇编语言,可以作为高级语言的可移植编译目标(就是类似于.o
的文件),把高级语言编译成一堆二进制机器指令。编译成WebAssembly的话好处大概是可以跨平台?现在有很多浏览器,以及Node.js都支持WebAssembly了。简而言之,它是一种在javascript的大部分可能的运行环境下都能被友好支持的汇编。
有一个工具Emscripten,可以专门把C/C++编译成WebAssembly机器指令。
而众所周知(不是x)cpython的解释器是C写的,诶嘿,它是不是也可以被编译成WebAssembly呢?这样一来,在javascript的大部分可能的运行环境下(例如浏览器,Node.js等),我们就有了一个python解释器!还不需要再额外装cpython了!换句话说,这部分WebAssembly码就是python的runtime了。而且,何止是javascript,如果有其他语言的runtime也支持了WebAssembly,那其他语言也可以在没有cpython的情况下跑python了!
做这件事的就是这个项目:Pyodide。可以把它理解为一个基于WebAssembly的python解释器。我们在cpython下可以用python a.py
运行python程序,是因为指令python
在操作系统上连接到了cpython的python解释器的可执行文件上。而安装pyodide之后,你也会有个类似的可执行程序,是pyodide的python解释器。
与前面两种方法相比,最大的优势在于完全脱离了cpython的runtime(不用装python了),这对于让浏览器支持跑python是很有意义的,其实基本原理Part 2:“用js写python解释器”的目标之一也是这个。
但与“用js写python解释器”相比,这种方式最大的区别在于:可以使用C编写的python包了!js解释器不支持的python包,通常原因是人家是用C写的,比如numpy
之类的,所以js解释器执行不了。但用C写又怎么样,照样可以编译成WebAssembly,从而被使用。只不过,需要pyodide
的开发者每次都把这些包手动从C源码编译成WebAssembly库。目前他们已经编译了一些常见的基于C的python库(比如numpy
)并内置了,安装pyodide
就能用。
个人感觉前两种原理做不到的事情它都可以,只要能成功构建(build)好环境……如果用的是docker,能解决操作系统(OS)和体系架构(Architecture)带来的烦恼的话,这种方式无敌了。
这一期内容比较多,简单做个总结吧。通过C/C++联通python和javascript有两种思路:
最后让我用之前外语学习的破烂比喻收尾吧(没有冒犯的意味):
转 https://www.cnblogs.com/milliele/p/15808609.html
以上是脚本宝典为你收集整理的Python与Javascript相互调用超详细讲解(2022年1月最新)(三)基本原理Part 3 - 通过C/C++联通全部内容,希望文章能够帮你解决Python与Javascript相互调用超详细讲解(2022年1月最新)(三)基本原理Part 3 - 通过C/C++联通所遇到的问题。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。