前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++协程库与嵌入V8的兼容性问题

C++协程库与嵌入V8的兼容性问题

原创
作者头像
h46incon
修改2019-01-23 12:38:03
1.7K0
修改2019-01-23 12:38:03
举报
文章被收录于专栏:知了知了

环境介绍

因为业务需求,需要在 C++ 中调用 js 代码,这里选择使用 V8 引擎。

C++ 中使用了部门自研的有一定历史的 RPC 框架,所绑定的协程库是 GNU pth。

开发时,在 PC 上进行编码,然后将代码同步到编译机上进行编译,再然后将编译出来的 so 文件上传到另外一台开发机上运行。

Bug 现象

框架在初始化时,会调用业务的 Init 函数。然后在请求时,再调用相应的业务接口函数。碰到的第一个问题是:

Init 函数初始化 V8 实例的话,在业务函数中对 V8 的调用都会返回失败。 但是,如果在业务函数中再初始化 V8 实例的话,就可以成功调用 V8。

如问题所述,这不是问题,就这样跑了一段时间。

随着业务的发展,增加了第二个需要使用 V8 的接口。一开始很直接的思路,就是使用单例 V8 引擎,然后在调用业务函数的时候再初始化 V8。这时碰到了问题:某些时候,其中的一个接口调用 V8 会失败,但重启服务后,就有可能会调用成功。经过多次尝试,归纳出以下现象:

如果在接口 A 中完成 V8 的初始化,那么接口 A 和接口 B 中使用 V8 都不会有问题。 如果在接口 B 中完成初始化,那么以后在接口 B 中一直能成功调用 V8,但在接口 A 中调用 V8 会必现失败。

其实挺难总结到这样鬼畜的行为的,因为当时所执行的 js 脚本也在不断开发修改,接口又是那种时灵时不灵的行为

Debug 过程

// TODO: 一般这节不会有人看,随便写写就好

协程库的问题?

因为框架使用了协程库,这是一个会用上各种奇技淫巧的地方,而且框架选用的协程库又是没什么人用的 GNU pth,所以嫌疑很大。这个协程库太小众了,以致于很难找到相关的介绍实现的资料,所以准备直接啃代码。幸运的是,啃了一会发现看不懂,但是发现了作者在发布时其实带上了 paper:

1.png
1.png

想想很刺激,论文是 .ps 格式的,我乡下来的完全不知道这是个文档。

此文介绍了怎么实现一个兼容性很强的协程栈(比如使用了软中断的回调创建协程……),然后得到的信息是:

协程库里用的是独立的协程栈。 没有移动协程栈的操作。

这是一个比较稳的协程栈实现方式,出问题的概率不大。

V8 的问题?

另一个方向是从 V8 下手。首先得到最小复现代码:v8::JSON::Parse(...) 。如果出问题了,那么这个简单的从 JSON 中构造 V8 对象的语句就会失败。

遗憾的是,英特网上的资料大多都是介绍 V8 怎么使用,很少介绍 V8 的实现。但是啃 V8 的代码不太现实,我稍微看了一下里面的代码后,就决定痛改前非,尽量不要使用宏实现控制流。

V8 的编译

然后很直接的思路就是跟着调试器走。但之前的哥们编译好的 V8 静态库里没有带符号表,这超出了我的调试水平。所以需要自己重新编译一份带符号表的。编译又是一个坑,给我的感觉是,凡是需要编译 V8 的,都是那种不需要资料也不屑于写资料的人。

  • 编译选项

如果编译选项没选对的话,也能成功编译,但运行时会报错。比如默认的选项是支持 snapshot,但是这样编出来的库,如果运行时不带相关的快照文件的话,就会初始化失败。再比如默认使用的 binutils,以及 thin lto 格式,编出来的静态库使用起来不太友好。

这里贴一下编译选项,万一有人也要踩这个坑的时候用得上(适用于 6.2.414.46 版本):

(见文末)

  • 符号表用的是相对路径

另一个坑是编译 V8 使用 ninja,编出来的库所带的 debug 源文件信息,用的都是相对路径。前面说到,我们又要使用编译机,又要使用开发机的。而且在这种代码量庞大而且不熟悉的请求下,在 PC 开着 IDE 使用远程 GDB 才是正确的选项。所以我们需要让 debug 信息里带上绝对路径。

当时没有找到什么比较简便的直接修改符号表的方法,所以选择了 wrap gcc 的调用,将调用 gcc 时的相对路径的参数转成绝对路径后,再真正调用 gcc。

这里写了一个通用的脚本实现这个转化:

(见文末)

使用的时候,建立一个所需文件名到这个脚本的链接,然后设置好 PATH 路径就好了。

这样编译好静态库之后,就可以正常与业务代码进行链接、调试了。可以进行 Debug 之后,对这种必现的单线程 Bug,问题不难发现。

结论及解决

如一开始说的,问题就是 V8 认为发生了栈溢出:

3.png
3.png

结合前面说的协程栈实现,很容易就想到可能是因为协程栈地址的问题。这里再观察下 V8 成功、失败的协程栈地址就可以确认,不再赘述。

V8 和协程库,都不会想到还有这样的队友,导致了(我的)悲剧的发生。

但是还好这个兼容性问题要绕过不难。这里再解释下一开始说的 Bug 现象,即接口 A 里初始化 V8 的话,接口 A 和接口 B都能使用这个 V8 实例。原因和 AppPlatform 对协程的管理方式有关:

  • 每个接口对应一个协程数组,调度时,总是从数组的第一个元素(最低地址)开始,选择一个空闲的协程。
  • 接口 A 比接口 B 先分配协程数组,由于堆空间地址是往上生长的,导致接口 A 的协程栈地址均小于接口 B 的。

所以在接口 A 的第一个请求里初始化 V8 实例的话,其所记录下来的栈地址,一定是这两组接口里相对很低的地址。栈空间是向下生长的,V8 判断栈溢出的方法是判断当前栈顶地址是否小于初始栈地址 - 某个阈值。所以后面运行的时候都不会触发这个溢出判断。

根据这个特性,可以想到一个很简单的绕过方法:在每个接口里都分别初始化一个 V8 实例。使用这个方法也的确工作了一段时间。但这种依赖框架实现细节的做法是明显不靠谱的,所以需要更彻底的解决。

浏览 API 后发现其实 V8 有设置栈阈值的功能:

  • 方法 1

一个是在运行的时候,动态设置栈阈值:

代码语言:txt
复制
v8::Locker isolateLock(isolate);
isolate->SetStackLimit(currentStackLimit)

这里又有个坑,是在调用这个函数的时候,需要加一个锁,否则它只会修改 C++ 栈阈值,而不会修改 js 的栈阈值,同样会导致栈溢出。但这里的锁可能会带来一定的性能开销。

  • 方法 2

另一个是在初始化 V8 实例的时候,设置一个非常低的栈阈值:

代码语言:txt
复制
create_params.constraints.set_stack_limit((uint32_t*)0x1);
// ...

但这样就等于是放弃了 V8 的栈溢出检查。

  • 方法 3 使用 Copy Stack 的协程,如 libco 。

选哪种方法,就自行取舍吧。

V8 编译选项

代码语言:txt
复制
## V8 编译时的 gn args

target_cpu = "x64"
# release
is_debug = false
symbol_level = 1

# debug
#is_debug = true
#v8_enable_backtrace = true
#v8_enable_slow_dchecks = true
#v8_optimized_debug = false
#use_debug_fission = false
#symbol_level = 2

# compile
is_clang = false
use_custom_libcxx = false
use_custom_libcxx_for_host = false
use_cxx11 = true
binutils_path = "/usr/bin"
use_sysroot = false
use_rtti = true
use_thin_lto = false
linux_use_bundled_binutils = false
is_component_build = false
v8_static_library = true

# features
is_desktop_linux = false
use_glib = false
use_aura = false
use_pic = true
use_gconf = false
use_ozone = false
use_udev = false
v8_enable_i18n_support = false
v8_use_snapshot = false
v8_enable_gdbjit = false
icu_use_data_file = false

相对路径参数转绝对路径

代码语言:txt
复制
#!/bin/env python
# -*- coding: UTF-8 -*-
# FileName: AbsPathMap.py

import sys
import subprocess
import os

#--- exec name
exec_name = os.path.basename(sys.argv[0])
# print(exec_name)

#--- find real exec path
search_paths = os.environ['PATH']

cur_dir = os.path.normpath(os.path.realpath(os.path.dirname(sys.argv[0])))
# print('cur_dir: ' + cur_dir)
real_exec_path = None
for path in os.environ['PATH'].split(':'):
  rpath = os.path.normpath(os.path.realpath(path))
  if rpath == cur_dir:
    continue

  fname = os.path.join(rpath, exec_name)
  if os.path.isfile(fname):
    real_exec_path = fname
    break

if real_exec_path is None:
  print ('Could not find true exec file: %s' % exec_name)
  exit(-1)

# print ('real_exec: ' + real_exec_path)

#--- translate to absolute path
new_args = [real_exec_path]
for arg in sys.argv[1:]:
  if len(arg) == 0 or arg[0] == '-' or arg[0] == '@':
    new_args.append(arg)
    continue
  new_args.append(os.path.abspath(arg))

# print new_args
#--- call
subprocess.call(new_args)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 环境介绍
  • Bug 现象
  • Debug 过程
    • 协程库的问题?
      • V8 的问题?
      • 结论及解决
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档