目前网上找到的ios嵌入nodejs介绍,都是指向nodejs-mobile项目,nodejs-mobile对nodejs项目做了一定魔改,可以预想会难以及时的随nodejs升级,该项目目前的nodejs版本12.19.0,比起官方版本落后太多。而本文介绍的办法只需对nodejs的gyp添加少些修改以支持ios、android的编译,该方式编译的16.16.0版本nodejs已经在真机上测试通过并应用到puerts项目上。而且该修改方式也已经提PR给nodejs官方并合入到主干: libnode for ios app embedding
尽管我们反复的解释了nodejs是“JavaScript Runtime”之一,如果一个js库用了nodejs专有的api是不能直接在其它“JavaScript Runtime”,比如browser,puerts里运行的。
但奈何nodejs已经事实上约等于js,用puerts的童靴有时候找资料,找到的“如何用ts/js完成XXX”系列文章往往都是nodejs的。很自然的跑过来问puerts为啥不行,不是说支持js么?
其次puerts项目的出发点之一就是把繁荣的js生态引入到游戏开发,如果缺失了nodejs生态,这将是一个重大遗憾。
于是在去年,puerts就尝试在桌面平台支持nodejs脚本后端:《UE引擎里头跑个nodejs服务器是怎样一种体验?》 、【PuerTS】我们把Node.js放进了Unity里(一),但由于移动平台的缺失,我们只是推荐用在做引擎编辑器扩展开发。
不过发现还是有些没有发移动端需求的项目用了nodejs版本,而且反馈对他们开发十分有帮助:网络方面比UE提供的好用,除了界面相关,其它需求都很容易找到相应的nodejs组件。而界面相关又恰好是游戏引擎的强项,所谓强强联合了。于是我对移动端nodejs的支持更期待了,但nodejs并没有移动端的官方支持,特别是ios。
我在网上找移动端nodejs的支持情况,ios只找到nodejs-mobile,它支持nodejs版本远低于我们要求的版本,并不适用,而android下发现官方提供了个android-configure脚本,既然官方提供的,我想应该是能用的吧?但事实上没那么简单,一个接一个的坑,所幸最终还是搞定了,于是实现了puerts对nodejs的双引擎(UE、Unity)×多平台(Window,Mac,Linux,iOS、Android)支持。
具体做了那些修改就不细说了,这些修改都以git patch的形式放到这个项目,可自行查阅:https://github.com/puerts/backend-nodejs
相关的资料不多,可以看看我之前两篇相关文章:
nodejs本身除了GUI大多数任务都能完成,于是有人把chrome和nodejs缝合,做成个跨平台的桌面框架electron,十分火爆,这里是它的showcase ,我们程序员耳熟能详的VSCode也赫然此列。
如果把chrome换成专业的游戏引擎:UE、Unity。从2维升级到3维,而且除了桌面平台,还支持移动平台,甚至主机平台,是不是很有想象空间呢?期望有人能把它搞起来。
接下来的章节记录的是探索nodejs移动平台时踩过的坑,可以跳过,直接翻到文章结尾有现成编译好的全平台libnode。如果你个人需要定制什么编译参数,需要自行编译可以再来翻看。
我抱着试试看的心态用android-configure编译了一下arm64的libnode.so(用的是puerts当时用的nodejs版本14.16.0),竟然顺利编译成功,习惯了困难模式的我隐隐觉得事情没那么简单。
放到Unity版本的puerts在真机上测试,果然失败了,提示libnode.so失败。按经验应该是libnode.so依赖的某些库缺失。 用ndk提供的工具查看依赖
aarch64-linux-android-readelf -d libnode.so
对比下v8版本的puerts的依赖
发现多了个libc++_shared.so,感觉应该是这个导致的,回头查看nodejs的编译选项,发现--partly-static可能可以解决这个问题。编译后libc++_shared.so确实没了,上真机测试果然能正常跑了!!!
感觉更难的arm64都编译通过了,arm应该更简单,没想我还是天真了,碰到两个问题
nodejs的最主要部分:V8我们在iOS已经应用了很久(加--jitless选项)。而android-configure的存在,也证明了其它部分在arm架构下运行问题不大。推测iOS嵌入nodejs最大的门槛在编译。
我尝试参考android的交叉编译做了一版iOS的交叉编译,结果失败了。
其生成的Makefile根本没法使用,我尝试去看gyp的代码,尝试调整Makefile的输入,仍然是失败的。
虽然失败,但也有收获,而且该收获直接导致后面成功:我初步搞懂了gyp是啥,它是一个python写的程序,该程序会根据gyp的配置生成编译工程:window下的vs工程,linux/unix下Makefile等等。gyp配置解析部分是通用的,然后调用一个个generator(msvs.py,android.py,make.py)去生成工程。
iOS(unix系OS)调用的generator是make.py,但make很复杂,我也不熟悉,搞不定。于是把目光投到其它generator,最终锁定ninja。
ninja的规则十分简单(十分钟能学完),有问题比较容易找到并调整,借助它我最终把nodejs的ios交叉编译调通了。
当然,也不是一帆风顺,期间也碰到几个问题:
苹果的动态库发布很麻烦,需要签名什么的。我们更希望以静态库提供。于是尝试编译静态库。把configure的--shared改为--enable-static即可,编译也很顺利,但链接找不到符号,这些符号位于这两个文件:http://node_snapshot_stub.cc,http://node_code_cache_stub.cc。构建libnode.so会包含,我们把这两也变成静态库链接即可。
这次很顺利,搞定了编译,用unity应用在iOS上测试直接就通过了。
搞定unity,ue上跑还算顺利,只是碰到一个问题:ue和nodejs都用了openssl,但ue的版本老,两种冲突了。表现为:
最终我先把nodejs的openssl去掉(加--without-ssl选项),如果自行编译ue引擎的童靴也可以选择保留nodejs的openssl,升级ue引擎的openssl。
我们已经把libnode的全平台编译做成github action自动化编译:https://github.com/puerts/backend-nodejs ,也有编译好版本,可以直接使用:有带openssl的版本 ,也有不带openssl的版本 。