C/C++脚本化: 探索篇

C/C++脚本化: 探索篇

本文来源:Zero’s Donkey Den
原文地址:7-0.cc/cpp_scripting_exploration_1
转载许可:未经授权,禁止转载

使用C/C++等系统级语言开发业务逻辑效率太低? 这个功能用脚本写最多只要三天?我们对执行速度和资源又没啥要求…… 不知道过去你是否也有过这样的想法?

随着软件工程与计算机硬件的飞速发展,对于一些开发场景来说,系统级语言多少显得有点“大材小用”了,这迫使我们需要结合一门脚本语言相互配合,快速迭代产品。

本文主要描述通过脚本语言来扩展C/C++,而非以脚本语言为主的应用场景。

C/C++脚本化: 探索篇

1 为什么要脚本化

我们知道C/C++是一种高效且功能强大的系统级编程语言, 被广泛用于操作系统、服务器、游戏引擎及客户端应用程序的开发。然而随着计算机硬件性能的飞速提升,在许多对性能要求不高的场景下,相比其所带来的复杂度与开发成本,其他方面的优势已经不再显著。许多开发者转向了脚本语言,利用其与生俱来的灵活性、丰富的生态,快速迭代产品,达到降本增效的目的,而需要性能的场景又可以反过来结合C/C++,这样即保证了程序的高效运行又兼顾了开发速度。

比如近些年流行的 Electron, 它将WEB技术应用到了桌面端应用程序, 即用JavaScript写业务逻辑, 以HTML、CSS渲染用户界面。

另外一些开发者期望将程序的相关功能,通过脚本语言的接口暴露给用户,以便用户可以通过这些接口来扩展程序的功能,提高灵活性以及适应性。如IDA静态分析工具就使用Python来扩展软件功能,这样用户可以通过Python编写插件、执行一些自定义任务。此外还有Microsoft OfficeVisual Basi、《魔兽世界》与Lua等等。

2 现有项目的脚本化及方式

尽管与脚本语言结合有诸多优势,但现有大部分的项目仍然是通过传统C/C++实现。因此针对这部分现有项目,我们可以在其版本迭代、新功能开发、维护期间,通过脚本化部分功能模块或业务逻辑,达到简化开发与维护的工作,即某项独立的任务通过调用脚本语言编写的模块来完成。

这里我们主要探讨脚本语言扩展C/C++的场景,目前主要有如下两种扩展方式:

  1. 直接嵌入脚本语言的解释器
  2. 通过子程序的方式调用脚本模块(不适用于Lua)

2.1 嵌入解释器

将脚本语言解释器作为一个模块集成到C/C++项目中,然后以调用函数的方式运行脚本模块,通常需要等待脚本运行结束之后才会返回。执行结果通过解释器API获取,通常包含:返回值上下文对象

2.1.1 优点
  • 灵活性高
    通过解释器提供的API,开发人员可以轻松修改和扩展解释器的功能。
  • 交互简单
    脚本与宿主程序运行在同一进程内,两者之间可以通过暴露接口的方式直接交互,而无需编写进程通讯方面的代码。
  • 资源共享
    由于在同一进程内,资源可以相互共享(通过接口访问)。
2.1.2 缺点
  • 兼容性风险
    不同发行版的解释器提供的API可能并不兼容,切换解释器版本困难。
  • 并行执行差
    大部分脚本语言解释器是单线程设计,不能提供真正意义上的多核并行,且这种机制不能通过在多个逻辑线程上运行解释器解决,如CPython的全局解释器锁(GIL)[1]
2.1.3 例子

下面是通过Boost.Python嵌入Python解释器[2],并执行了一个简单的脚本文件的例子

script.py:

print('Hello World !')
number = 42

embedding.cpp:

int main(int argc, char **argv)
{
  namespace python = boost::python;

  // Initialize the interpreter
  Py_Initialize();

  // Retrieve the main module
  python::object main = python::import("__main__");
  
  // Retrieve the main module's namespace
  python::object global(main.attr("__dict__"));

  // Run a python script in an global environment.
  python::object result = python::exec_file("script.py", global);

  // Extract an object the script stored in the global dictionary.
  BOOST_TEST(python::extract<int>(global["number"]) ==  42);

  return 0;
}

2.2 子程序调用脚本模块

这种方式同命令行终端中运行脚本是一样的,都是通过创建子进程来运行脚本模块。但该方式不适用于Lua,因为Lua的定位是嵌入式脚本语言,需要嵌入到宿主程序才能发挥作用。

2.2.1 优点
  • 并行执行方便
    通过多进程并行,可以很好的利用多核优势。
  • 版本兼容性好
    可以在不同解释器发行版之间快速切换,无需担心API兼容性问题,能够快速的升级至新版本。
  • 稳定性好
    尽管脚本语言很少像C/C++一样,在遇到程序缺陷时发生崩溃,但多进程结构的稳定性本身也是一种优势。
2.2.2 缺点
  • 灵活性差
    相对于直接嵌入解释器的方式来说,此种方式较难以修改和扩展解释器。比如我们期望将程序的某个功能能在脚本中的使用,那么需要将其包装为该解释器的扩展,而不是通过API直接向解释器注册。如果存在很多这样的需求,那这是一个艰巨的任务ಠᴗಠ
  • 交互复杂
    由于脚本与宿主程序不在同一进程内,两者之间的交互需要跨进程通讯,具体的通讯方式需要开发人员视业务场景自行选择,不过一般存在如下几种:
    • 通过子进程的标准输入、输出流,该方式实现简单且跨平台,但很多模块都会使用标准输入、输出,精确控制有一定难度。
    • 通过Socket网络通讯,任何语言都可以方便地处理网络,实现相对容易,跨平台性非常好。
    • 通过命名管道、消息队列、共享内存等常见IPC技术,这种方式不容易跨平台或语言,通常需要为不同的系统、语言编写代码。
2.2.3 例子

下面的例子是通过Boost.Process [3] 将Python脚本文件作为子进程运行,读取并存储子进程标准输出流中的内容,待其运行结束后再打印出来。

script.py:

print('Hello World !')
number = 42

subprocess.cpp:

int main(int argc, char **argv)
{
  namespace bp = boost::process;

  bp::ipstream is; //reading pipe-stream
  bp::child c(bp::search_path("python script.py"), file, bp::std_out > is);

  std::vector<std::string> data;
  std::string line;

  while (c.running() && std::getline(is, line) && !line.empty())
      data.push_back(line);
  c.wait();

  // 输出: Hello World !
  for (const auto& i : data)
    std::cout << i << std::endl;

  return 0;
}

2.3 总结

通过以上,我们可以看出两种方式的侧重不同:前者简单灵活,可以对解释器灵活控制,但缺乏并行机制。后者虽然弥补了前者的缺陷,但却带来了新的复杂度。

在实际的开发过程中,我们可以根据实际情况,选择其一或搭配使用。比如:

  1. 需要与宿主程序强耦合或对解释器深度定制的场景,推荐嵌入解释器方案。
  2. 需要异步、并行处理的场景,推荐子程序方案。
  3. 如果以上场景都会涉及,推荐通过嵌入解释器并包装成一个可执行程序,然后以子程序的方式调用(两种方案结合使用)。

3 脚本语言的选择

  • Lua [4]
    Lua的目标是轻量级嵌入式脚本语言,特点是快速、简洁、轻量级,但缺点是标准库、生态都相对较小,据说它的执行速度是所有脚本语言最快的。基于此特性很多游戏客户端、服务器使用Lua作为脚本化解决方案。
  • Python
    Python语法简洁、清晰、易于上手,并且拥有广泛而丰富的生态和第三方库,通过Boost.Python库可以很方便的与C++集成,但执行速度较慢。
  • JavaScript
    JavaScript语法简单、灵活、执行速度快,有丰富的生态与第三方库。被广泛应用于Web开发,许多开发者或多或少都曾使用过这门语言,无论他是否为Web开发者。

建议结合业务实际情况,选择一种即可,如果看中脚本语言的生态,不在乎执行速度则建议使用Python。如果在乎执行效率则建议采用Lua,择中则考虑JavaScript。如果实在不知道选择什么好,可以试试你掌握的最多的那一个。

结语

以上是个人在桌面端应用程序开发迭代中的一些思考和感悟,恐有不实之处,就权当抛砖引玉。如能给你带来一点收获,我将倍感荣幸。

参考资料

[1] Python/C API Reference Manual: Thread State and the Global Interpreter Lock
[2] Boost.Python: Using the interpreter
[3] Boost.Process: Synchronous I/O
[4] Extending a C++ Application with Lua 5.2

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值