Jump to Navigation

Qt库的ruby语言绑定的探索与实现过程笔记

# Qt库的ruby语言绑定的探索与实现过程笔记

一直一来我经常使用Qt做一些简单的linux桌面工具软件,也算是对Qt库比较熟悉了。
而真正开始搞Qt库的ruby语言的绑定,出于当时觉得ruby是个比较有意思的编程语言,并开始接触学习ruby语言。
虽然在ruby最热门的时期作为web快速开发框架面世的,实现上我认为ruby也是一种非常不错的系统脚本语言。

在不断深入的学习ruby语言的过程中,越来越希望所有的开发工作都使用ruby语言实现,
并且这不只是希望,也尽可能的使用ruby完成一些开发工作。

不过,在这时,遇到一个需要更复杂的项目,希望能为这项目开发GUI界面功能。
经过一番网上搜索,也发现其实已经有qt的ruby语言绑定相关库了,
像kde项目中的rubyqt,还有qtbindings这个gem包,这两个包也比较成熟稳定了。
在试用的过程中了解到这两个包其实是同源的,并且都只支持Qt4版本。

由于qt5已经发布1,2年时间,也已经比较习惯用qt5开发了,所有用起来的时候总想着怎么让它们支持qt5。
这期间也试着对这两个项目做些升级工作,却发现也不是那么容易的。
因为qt5中大量使用了C++11新语法,新功能,而这两个项目的C++解析器很难完全处理这些新语法和新功能,
对于想让它们支持qt5的想法和尝试,最终以失败告终了。

虽然这个尝试失败了,不过在这期间,自己也考虑并尝试了很多方式,并且最终获得一种我认为非常好的实现方式。
由于这种方式与现有的实现方式是如此不同,并且让我学习到了很多,决定把这整个过程整理记录下来,
以备后续查阅,同时也为有兴趣的同行们抛砖引玉,实现更完善易用的ruby/qt开发库。

整个尝试并找到一个合适的方案的过程非常曲折,再加上本人写作水平有限,有描述不清晰还请轻拍。

### 第一阶段,手工编写ruby的C/C++扩展

这一阶段,是最开始的简单想法。当时考虑的是在编写程序的时候,遇到哪些用到的方法,就把它们手工添加到扩展中。
当然也试着编写了几个类的扩展,并且简单测试后确定该方式的可用性上也完全没有问题,效率也比较高。

这种实现方式大概需要几个步骤,
使用rb\_define\_class注册一个类到ruby运行时。
使用rb\_define\_method为新注册的类注册一个方法。
如果有多个方法的时候,则多次调用rb\_define\_method。
分别实现注册过的封装方法,调用C++的方法实现真实调用。

简单直白是这种方式的最大特点,所有的Qt类名与方法名都以明文的方式出现在扩展程序代码中,
容易编写实现,也容易阅读理解,是一种非常简单粗暴甚至好用的实现方式。

本着这思路,开始用扩展编写程序了,却发现来回为扩展添加类或者方法,再回头编写目标程序
来回切换实在实际使用时非常不理想,编写目标程序思路不断打断,而添加扩展类功能也像
是机械一样不断输入大量重复代码,越来越难以承受在这上面花费的机械而重复的工作了。

比如,对于每个类,都需要有构造方法和析构方法,而每个类的这些方法实际上非常相似,
但又不能说完全一样。即使能够采用一些变量或者参数的方式实现成一样,也不免要依赖
一些动态性质的编码了。

在这种状态下纠结着写了部分类的扩展之后,实在是难以忍受了。大概总结是,qt中有上千个类,
每个类又会有20,30甚至更多的方法,工作量之大已经到了无法实现的地步了。
思来想去,还是编写一个程序,来生成这些大量重复的代码好了,这种方式蒋在下一节中进行详细探索。

虽然在这花费了许多时间,也不是完全没有收获的,在这个尝试过程中逐渐对以下几个方面有了进一步的理解:
熟悉了编写ruby扩展的基本方法。
总结出了会包含大量重复代码的点。
对整体工作量有大体概念。
对最终要产生的扩展目标有了更清晰的概念。

### 第二阶段,编写代码生成工具,自动生成部分重复代码

### 第三阶段,使用swig的语言绑定包

### 第四阶段,使用Qt meta机制,实现动态的方法调用

### 第五阶段,预告整理出Qt共享库中的C++ symbol并执行

### 第六阶段,动态解析并匹配Qt共享库中的C++ symbol并执行

### 第七阶段,使用clang自动把生成的C++绑定代码编译为执行代码

### 第八阶段,手工翻译生成IR代码,使用LLVM的MCJIT执行

### 第九阶段,使用clang自动把C++ inline方法翻译为IR代码,并使用LLVM的MCJIT执行

感觉真像历经九九八十一难,终于看到光明了啊。

这种方式看上去只是把上一阶段的IR代码生成方式改为自动的了,而其际上却是非常大的改进。
因为这相当于多使用了编译器的前端模块,用于把程序从源代码转换为AST语法树,生成中间代码,
这部分模块才算是编译器的真正核心。

下面就把这种方式在ruby-jit-qt中实现的部分加以更详细的说明。
首先要说明的一点,现在的实现需要一个预先生成好的AST树,省略掉AST语法树动态生成一步,
而只是使用了通过AST语法树生成IR中间代码的功能。选择这个方式主要是基于效率和复杂度两方面的考虑,
如果每次调用都从源码生成AST语法树速度上比较慢,对于像Qt这种复杂的C++尤其慢。
对于AST语法树的构建,当然还有另一种模式,那就是动态增量式AST语法树构建。应该可行,
但是还没有搞太清楚如何实现,可能会非常复杂,还有就是这不过是把预先生成花费的时间,
插入到了运行时处理,虽然动态化程序提高,灵活性也更高,但整体运行耗时实际应该会更长。
所以,现阶段选择了相对适中的预先生成实现方式,先把关注点放在IR中间代码的生成上。

预先生成AST语法树功能,现在的实现是,把所有的Qt头文件#include进一个头文件,
通过clang++命令行的-S -emit-ast生成.ast文件,在运行时使用clang::ASTUnit动态加载,
然后在有qt调用时,动态遍历这个AST树,查找像类、方法、函数、常量的声明和定义,
并通过CodeGen模块中的相关类把inline类型的函数和方法,甚至需要把inline类型的模板方法展开,
统一转换为IR中间代码,传递给llvm的JIT后端执行。
注意,这里其实只关注的是inline类型的函数、方法或者模板方法。因为对于C++库来说,inline类型的函数和类方法,
实际上不在共享库.so或者静态库.a中,实际上只是存在于头文件中的C++源代码,并由编译器在编译时通过头文件动态生成的代码,
插入到调用inline调用位置,以生成更高效的最终代码。在该项目的实现中,必然也需要使用这种模式。

生成了AST语法树之后,在真正进入JIT引擎执行之前,还有一步比较重要的操作,把当前内存空间的变量绑定到
JIT环境中的变量上,当JIT执行IR代码时,才能真正实现JIT中的IR与当前程序空间的变量交互,
完成该段IR代码的功能才真正体现。
在当前的实现中,是通过把当前要传递到JIT空间的变量的地址绑定到一个IR变量,在封装一个简单的调用Qt方法的IR函数实现的。
在绑定实现过程中,有一些比较需要注意的细节,像对不同变量类型的处理方式,对不同类型返回值的解析等,
这儿就不再做详细的描述,准备后续在更详细的针对每个技术点的文档作说明。

最后一步就是该在JIT引擎中执行IR代码了,正如上一段提到的,还需要一个IR入口函数,
这个函数封装了绑定到JIT空间的C++进程空间变量,然后以IR的方式调用了真正的Qt方法的IR代码。
现在实现以比较简单的机制实现的JIT执行,即每次执行都会创建一新的JIT执行引擎,
所以这个入口函数也比较简单,实际上它的函数原型对所有的调用都是一样的,以此简化当前的实现。
入口IR函数的原型为: define void* @jit\_main() {}
在后续的优化中,这个IR函数原型可能实现为不一样的形式,至少是不一样的名字,以实现IR代码利用的目的。

除了以上提到的核心机制,还有一些详细的小的实现机制,搭配起来才最终实现这种模式的Qt库绑定调用。
以上的提纲式说明,希望能给理解实际系统一点全局上的概念,以便更好的理解实现细节。

本段收获小结:
熟悉了AST语法树结构,遍历查找相关知识。
熟悉了CodeGen模块生成简单代码的知识。
熟悉了编译器编译过程的更详细的步骤。
对C++模板编译时实例化机制有了更深入的理解。

### 后记

从开始着手,到实现一个能执行的原型,一直到现在完成了大部分的功能,一直抱着学习态度进行开发,
虽然现在没有达到优秀项目的程度,还是收获颇多,我心甚慰。
我也深知项目还有太多能够改进的地方,即使有些地方已经考虑到了一些方案,限于时间原因,
无法很快把想法和方案投入实践求证。
不过我还会尽量抽出时间,不断完善这些想法,不断改进这个项目质量,非常希望有一天,
这项目能从实验阶段进入到实用阶段啊。
如果有看到这个项目并有兴趣的同行,可以在项目主页https://github.com/kitech/ruby-jit-qt/上联系。

Category:

添加新评论

Plain text

  • 不允许HTML标记。
  • 自动将网址与电子邮件地址转变为链接。
  • 自动断行和分段。
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.


Main menu 2

Story | by Dr. Radut