PyNode是一个轻量级的Node.js C++扩展包,使用Node.js的N-API写成的,能在同一个进程里通过底层C/C++的API实现python和javascript的互操作,只需要进行数据类型的转换,运行效率高。详细的原理讲解可以看我这篇介绍。

本文主要简单记录一下使用PyNode的一些实践经验。

前提

安装的前提是装好两种语言的runtime。

  • Node.js:没啥特别的,直接装就行了。

    Linux系统安装Node.js可以直接用NVM(Node Version Manager),与python的conda类似。

  • Python:由于PyNode是在Node.js的C++扩展里嵌入了python,因此需要Python的动态/静态库

    一般情况下,cpython官方给的安装版都是由一个动态库(比如在Linux里会叫:libpython3.x.so),和一个依赖该库的小可执行文件组成。这样非常方便其它C++程序嵌入python。

    这种Python通常是通过--enable-shared选项编译安装的,可以在你的python环境里运行:
    import sysconfig
    sysconfig.get_config_vars('Py_ENABLE_SHARED')

    这个返回[1]就是true,[0]就是false。true代表着是通过--enable-shared选项编译安装的。

    还有一种更本质的方法,直接看可执行文件依赖的动态库。Linux上可以通过ldd命令,Mac OS上可以通过otools -L命令,查看你的python可执行文件的依赖情况:

    # If your python command is "python3", use `which python3`
    # On Linux:
    ldd `which python`
    # On Mac OS
    otools -L `which python`

    出现结果例如:

    # On Linux
    root@7fe6daacb730:/# ldd `which python`
    linux-vdso.so.1 (0x00007ffe41b47000)
    libpython3.9.so.1.0 => /usr/local/lib/libpython3.9.so.1.0 (0x00007fd5feb2c000) # python library
    libcrypt.so.1 => /lib/x86_64-linux-gnu/libcrypt.so.1 (0x00007fd5feaf2000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fd5fead1000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fd5feacc000)
    libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007fd5feac7000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fd5fe944000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd5fe781000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fd5fef08000) # On Mac OS
    /your/python/executable/dir/venv/bin/python:
    /Library/Frameworks/Python.framework/Versions/3.9/Python (compatibility version 3.9.0, current version 3.9.0) # Depends on python shared library
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1292.0.0)

    说明你的python有动态库,那么PyNode的安装应该不会出现啥问题。

安装

首先,如果python库没啥问题,按项目简介所说,安装前首先确认好你需要使用的python环境,如果要用虚拟环境,也先进入虚拟环境

# Install gyp-next
pip install gyp-next # Install PyNode
npm install @fridgerator/pynode
# or
yarn add @fridgerator/pynode

那么,如果你的python不依赖动态库咋办呢?如果你是Conda环境,那么还是有可能找到动态库的:

找到你的Python动态库

适用于Conda环境。一些详细的log可以参考这个Issue。下面的介绍跟我在这个Issue里的comment基本一样的。

如果你用的是Conda环境,恭喜你,他们提供的python,旧一点的,可能会遇到GCC版本低问题,而新一点的,已经直接用静态库build成可执行文件了,再也不依赖动态库了:

(py38) $ ldd `which python`

linux-vdso.so.1 (0x00007ffe03efd000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007faa187be000)
libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007faa185bb000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007faa1821d000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007faa17ffe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007faa17c0d000)
/lib64/ld-linux-x86-64.so.2 (0x00007faa18d5d000)

不过好消息是,Conda声称他们尽管不用,但还是提供了动态库,所以我们只要找到它就可以了。有一个很好用的python包find_libpython,能很好地找到你的python动态库:

(py38) $ pip install find_libpython
(py38) $ find_libpython # or "python -m find_libpython" /xxx/miniconda3/envs/py38/lib/libpython3.8.so.1.0.

然后我们要做的就是把动态库的目录路径作为库路径添加到PY_LIBS里(-L)。

但这样做链接的时候能找到库,后面执行的时候也还是会找不到,一种解决方法是用rpath把这个路径给他刻烟吸肺,运行的时候也能找到(通过-Wl,-rpath=)。那么加在一起,你的包安装命令就应该是:

PYTHON_SHARED=/xxx/miniconda3/lib PY_LIBS="$(python ../build_ldflags.py) -L$PYTHON_SHARED -Wl,-rpath=$PYTHON_SHARED" npm install @fridgerator/pynode

使用

没啥好说的,使用起来很简单,按项目首页的Readme做就可以了。列一些使用Tips和可能出问题的地方。

const pynode = require('@fridgerator/pynode')的时候动态链接错误

Uncaught:
Error: libpython3.9.so.1.0: cannot open shared object file: No such file or directory
at Object.Module._extensions..node (node:internal/modules/cjs/loader:1168:18)
at Module.load (node:internal/modules/cjs/loader:989:32)
at Function.Module._load (node:internal/modules/cjs/loader:829:14)
at Module.require (node:internal/modules/cjs/loader:1013:19)
at require (node:internal/modules/cjs/helpers:93:18) {
code: 'ERR_DLOPEN_FAILED'
}

根据StackOverflow上说,链接时能找到库是一回事,不代表它运行的时候也能找到,运行的时候查找动态库又有另一套路径(……)。

验证方法可以是:

cd进入node_modules/@fri/x/build/Release里,会找到编译好的PyNode.node

然后,ldd PyNode.node

  linux-vdso.so.1 (0x00007ffef7de3000)
libpython3.9.so.1.0 => not found # Failed to find when executing
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f976bd9c000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f976ba13000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f976b7fb000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f976b40a000)
/lib64/ld-linux-x86-64.so.2 (0x00007f976c1bd000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f976b06c000)

就是说,build的时候提供-lpython3.9-L<path_to_shared_library>是明确指定好库,这样生成的文件PyNode.node才包含这个库,换句话说,它才会出现在ldd的list里。

但这个可执行文件运行的时候找共享库,跟build的时候提供的路径没有关系,感觉它像是会在一个给定的路径集合里找,所以这些路径集合里没有libpython3.9.so.1.0,那ldd就会显示not found。

在linux下,一种解决方案是通过往环境变量LD_LIBRARY_PATH里添加这个动态库的目录,例如:如果动态库的绝对路径为/opt/libpath/libpython3.9.so.1.0,那么可以在~/.bashrc里添加一行:

export LD_LIBRARY_PATH="/opt/libpath:$LD_LIBRARY_PATH"

这之后再ldd .node文件,就能找到了:

linux-vdso.so.1 (0x00007ffc01562000)
libpython3.9.so.1.0 => /opt/libpath/libpython3.9.so.1.0 (0x00007f041a327000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f041a123000)
libstdc++.so.6 => /mnt/asp_test/env/miniconda3/lib/libstdc++.so.6 (0x00007f041a9be000)
libgcc_s.so.1 => /mnt/asp_test/env/miniconda3/lib/libgcc_s.so.1 (0x00007f041a9aa000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0419d32000)
libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f0419b2f000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f0419791000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f0419572000)
/lib64/ld-linux-x86-64.so.2 (0x00007f041a91d000)

另一种方法是用rpath,就像上面针对Conda环境安装的那样,把动态库的路径同时也加到-Wl,-rpath=里。

ImportError: math.cpython-39-x86_64-linux-gnu.so: undefined symbol: PyFloat_Type

或者类似undefined symbol: PyExc_ValueErrorundefined symbol: PyExc_SystemError的错误。

通常会在Linux上出现,属于是打开动态链接的时候出了问题。PyNode提供了一个dlOpen函数来手动打开动态链接。

  1. 首先肯定是要找到你的动态库,可以使用上文所说的find_libpython包来查找。假设为:
    /opt/libpath/libpython3.9.so.1.0
  2. 然后在Node.js里:
    const pynode = require('@fridgerator/pynode');
    pynode.dlOpen('/opt/libpath/libpython3.9.so.1.0');

在Node.js里运行Python的multiprocessing

Node.js通过C API起了一个python解释器,这种情况下本进程的可执行程序其实是node。换句话说,sys.argv[0]sys.executable都是指向node的可执行程序。

而python环境中开新的多进程同时运行,会获取当前的可执行程序(excutable),发现竟然不是python,那multiprocessing就读不懂这个可执行程序的信息,比如说找不到main从而报错。

查看源代码后发现了multiprocessing.spawn里的这么一段:

有一个接口set_executable,可以重新设置可执行程序。所以通过PyNode去调用使用了multiprocessing的python程序可以这么做:重新设置executable为你的python可执行程序

比如,我用了virtualenv,我的可执行程序就是:/my/project/path/venv/bin/python

const pynode = require('@fridgerator/pynode');
pynode.startInterpreter();
const sp = pynode.import('multiprocessing.spawn');
sp.get('set_executable').call('/my/project/path/venv/bin/python');
// Run the multiprocessing python code

需要注意的是,这种补丁操作只适用于纯Python的multiprocessing。如果你的某个子进程混入了一些node.js的代码,那么会报错。还没搞懂具体原理,我猜想原因可能是,子进程是通过python可执行程序起的,找不到node环境。

Jest单元测试卡住不会结束

表现是:

  1. 当测试文件多于cpu_count - 1的时候,整个测试程序卡住,不会再进行下一个test suite。
  2. 如果使用--runInBand选项禁止多线程执行单元测试,会Segment Fault

起因是segment fault了,而Jest不知道为啥在多线程跑单元测试的时候(一般一个文件一个线程,开线程的数量是cpu_count - 1),在某些OS里遇到segment fault当前线程就会卡死,所以总共会坚持cpu_count - 1个test suite,然后才卡死。如果文件数据少于cpu_count - 1的话,会顺利运行完。

经过研究,segment fault的原因是:StartInterpreter不可以重复调用2次,因为它里面有一个函数执行的时候需要Python的全局进程锁(GIL),但StartInterpreter在第二次调用的时候不调用Py_Initialize,因此也不重新获取GIL,导致segment fault了。

而Jest的每个测试的环境都是全新的,不管是把const pynode = require('@fridgerator/pynode');放到单独一个文件里require,还是用一个全局变量标记是否已经start Interpreter都无法阻止StartInterpreter被调用2次导致segment fault。

解决办法就是不用Jest 修改源码,不管怎样在调用那个函数前都获取一下GIL(PR)。

这个PR还没被merge进pynoe的版本,想安装这个版本的话,可以下载我fork的repo里的源码

然后在项目根目录里npm pack,会得到一个.tgz文件,然后在package.json里这么写:

"dependencies": {
"@fridgerator/pynode": "file:./pynode/fridgerator-pynode-0.5.2.tgz"
},

路径就是你的这个文件相对于package.json的路径。

Python与Javascript相互调用超详细讲解(四)使用PyNode进行Python与Node.js相互调用项(cai)目(keng)实(jing)践(yan)的更多相关文章

  1. Python与Javascript相互调用超详细讲解(2022年1月最新)(一)基本原理 Part 1 - 通过子进程和进程间通信(IPC)

    TL; DR 适用于: python和javascript的runtime(基本特指cpython[不是cython!]和Node.js)都装好了 副语言用了一些复杂的包(例如python用了nump ...

  2. Python与Javascript相互调用超详细讲解(2022年1月最新)(三)基本原理Part 3 - 通过C/C++联通

    目录 TL; DR python调javascript javascript调python 原理 基于Node.js的javascript调用python 从Node调用python函数 V8 嵌入P ...

  3. node.js 接口调用示例

    测试用例git地址(node.js部分):https://github.com/wuyongxian20/node-api.git 项目架构如下: controllers: 文件夹下为接口文件 log ...

  4. Python 基础学习笔记(超详细版)

    1.变量 python中变量很简单,不需要指定数据类型,直接使用等号定义就好.python变量里面存的是内存地址,也就是这个值存在内存里面的哪个地方,如果再把这个变量赋值给另一个变量,新的变量通过之前 ...

  5. Keras代码超详细讲解LSTM实现细节

    1.首先我们了解一下keras中的Embedding层:from keras.layers.embeddings import Embedding: Embedding参数如下: 输入尺寸:(batc ...

  6. 在 Node.js 上调用 WCF Web 服务

    摘要:有时我们需要在WCF中做一些复杂数据处理和逻辑判断等,这时候就需要在NodeJS中调用WCF服务获取数据,这篇文件介绍如何在Node中调用WCF服务获取数据. Node项目中调用WCF服务获取数 ...

  7. 现在学习 JavaScript 的哪种技术更好:Angular、jQuery 还是 Node.js?(转)

    本文选自<开发者头条>1 月 7 日最受欢迎文章 Top 3,感谢作者 @WEB资源网 分享. 欢迎分享:http://toutiao.io/contribute 这是一个发布在 Quor ...

  8. bootStrap-table服务器端后台分页的使用,以及自定义搜索框的实现,前端代码到数据查询超详细讲解

    关于分页,之前一直纯手写js代码来实现,最近又需要用到分页,找了好多最终确定bootstrap-table,正好前端页面用的是bootstrap. 首先下载BootStrap-table的js和CSS ...

  9. python beautiful soup库的超详细用法

    原文地址https://blog.csdn.net/love666666shen/article/details/77512353 参考文章https://cuiqingcai.com/1319.ht ...

随机推荐

  1. 【剑指Offer】滑动窗口的最大值 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 解题方法 暴力求解 单调递减队列 日期 题目地址:https://www ...

  2. libecc:一个可移植的椭圆曲线密码学库

    libecc:一个可移植的椭圆曲线密码学库 这段时间要写毕设关于椭圆曲线的部分,就参考了一个椭圆曲线库的代码来编写.这个库中的代码的结构.风格和封装在我看来是十分完善的.用起来也比较方便,当作一个密码 ...

  3. Electron 使用 Tray设置图标的路径问题

    问题报错信息如图 上面的代码在dev模式下不报错,但是在build后,安装后,运行会提示错误,错误信息的大意是参数错误,原因应该是安装后的图片文件路径有问题,这块没有详细研究解决上面的问题的方法,是使 ...

  4. Java实习生常规技术面试题每日十题Java基础(一)

    目录 1.Java 的 "一次编写,处处运行"如何实现? 2.描述JVM运行原理. 3.为什么Java没有全局变量? 4.说明一下public static void main(S ...

  5. Java Web程序设计笔记 • 【第8章 会话跟踪技术进阶】

    全部章节   >>>> 本章目录 8.1 Session机制 8.1.1 Session 简介 8.1.2 创建 HttpSession 实例 8.1.3 HttpSesiso ...

  6. Docker 安装并运行 Redis

    说明 在Windows下运行Redis主要有以下几种方式: 使用微软官方构建的Windows版Redis,最新版本是3.0.504,发布于2016-07-01.https://github.com/m ...

  7. vue 表格树 固定表头

    参考网上黄龙的表格树进行完善,并添加固定表头等的功能,目前是在iview的项目中实现,如果想在element中实现的话修改对应的元素标签及相关写法即可. <!-- @events @on-row ...

  8. mysql 外连接

    自连接:最大的特点是:一张表看做两张表.自己连接自己. 找出每个员工的上级领导,要求显示员工名和对应的领导名. select e.ename,ee.ename from emp e join emp ...

  9. 乒乓球队比赛,甲队有abc三人,乙队有xyz三人。 抽签得出比赛名单:a不和x比,c不和x,z比, 利用集合求出比赛名单

    import java.util.HashMap; import java.util.Map; /** * 乒乓球队比赛,甲队有abc三人,乙队有xyz三人. * 抽签得出比赛名单:a不和x比,c不和 ...

  10. Kube-OVN1.5.0新版本发布,支持鲲鹏云平台网络平面部署

    近日,Kube-OVN发布了最新的1.5.0版本.自2019年4月开源以来,Kube-OVN经历了15次重要版本迭代,以及社区成立,建设者贡献代码,稳定性测试,国内外用户开始在生产环境中投入使用,企业 ...