ObjC/Swift Cocoa 应用程序里嵌入 Ruby

luikore · 2012年10月31日 · 最后由 luikore 回复于 2012年11月29日 · 11145 次阅读
本帖已被管理员设置为精华贴

最近在 Cocoa 应用程序里嵌入 Ruby1.9 做文本相关的处理。由于 ObjC 和 Ruby 对象模型的相似之处,比以前在 wxWidgets 里和 C++ 交互爽多了,故来分享一下心得。

嵌入

嵌入 VM 的方式基本和镐头书里介绍的差不多,写了个 class 把相关代码放在一起。(Xcode 中要把 libruby.1.9.1-static.a 也包含进来,在项目配置的 header search paths 里加上 path-to-ruby/include/ruby-1.9.1)

#import "RubyVM.h"
#import "NSString+StringWithVALUE.h"
#include <ruby.h>

#define FORMAT(...) [NSString stringWithFormat:__VA_ARGS__]

RUBY_GLOBAL_SETUP;

@implementation RubyVM

+ (void)initVM
{
    int argc = 0;
    char** argv = NULL;
    ruby_sysinit(&argc, &argv);
    RUBY_INIT_STACK;
    ruby_init();

    // 这两个方法做的事情稍后解释
    [RubyVM my_init_loadpath];
    [RubyVM my_init_prelude];
    ruby_script("my_script_name");
}

#pragma mark - private

...

@end

RUBY_GLOBAL_SETUP, ruby_sysinit, RUBY_INIT_STACK, ruby_init 是不可缺少的步骤,无需解释。ruby_script() 用于设定脚本名字,虽然这个名字完全不影响 require 进来的脚本里 __FILE__ 的结果,但是如果不设一个名字,在使用 __FILE__ 的地方就会出错。

loadpath 和 prelude 需要一些额外的处理。


Ruby 的初始 loadpath 包含 Ruby 目录里的 lib/1.9.1/#{平台}, lib, site_ruby 等目录。但是很可惜的是,除 windows 外,这些路径是在编译 Ruby 之前就写死的 (距已故的 Guy Decoux 解释,是为了安全性... windows 反正本来不安全就不管了...), 调用 ruby_init_path() 的话会把这些写死的路径都放到 loadpath 中去。但是发布桌面应用不可能让全部用户都装一个 rvm, 标准库一般还是打包到应用程序的 Frameworks 目录里,下面 my_init_load_path 使用 Cocoa 的 NSBundle 工具把正确的 lib 路径找出来交给 Ruby.

+ (void)my_init_loadpath
{
    NSString* bundlePath = [[NSBundle mainBundle] bundlePath];
    NSString* script;

    // ruby_init_loadpath() hard coded load paths, need to roll our own
    NSString* libDir = [bundlePath stringByAppendingString:@"/Frameworks/ruby-lib"];
    script = FORMAT(@"$:.unshift '%@/x86_64-darwin12.0.0'\n"
                    @"$:.unshift '%@'", libDir, libDir);
    rb_eval_string([script cStringUsingEncoding:NSUTF8StringEncoding]);

    // prepare GEM_HOME for loading rubygems
    NSString* gemDir = [bundlePath stringByAppendingString:@"/Frameworks/ruby-gems"];
    script = FORMAT(@"ENV['GEM_HOME'] = '%@'", gemDir);
    rb_eval_string([script cStringUsingEncoding:NSUTF8StringEncoding]);
}

要在 XCode 中把标准库打包到发布程序中,可以参考这里添加一个 build step (由于我把标准库拷了一份到项目源代码下面,所以是从 $SRCROOT 而不是 rvm 目录开始复制)


Ruby 源码里是通过 ruby.c 中的 cmdline_options_init() 函数确定内外编码,然后才通过 Init_prelude() 载入转码数据库的。实际可以如下面 my_init_prelude 方法直接把内外编码都设成 UTF-8 即可。Ruby 的 Init_prelude() 函数搞定其余事情:定义 Mutex, 载入编码数据库,并且根据环境变量 GEM_HOME 初始化 RubyGems 的搜索位置。

#include <ruby/encoding.h>
extern void Init_prelude(void);

...

+ (void)my_init_prelude
{
    rb_encoding *enc = rb_utf8_encoding();
    rb_enc_set_default_external(rb_enc_from_encoding(enc));
    rb_enc_set_default_internal(rb_enc_from_encoding(enc));
    rb_define_module("Gem");
    Init_prelude();
}

GC 和对象的生命周期

嵌入 Ruby 时,GUI 对象的生命周期是尤其要注意的。例如一个文本框 NSTextField 被移除出窗体后,它的引用数变成 0 就回收了,但是包装了它的 Ruby 对象并不知情,一访问就 segfault 了。一个避免 segfault 方案是在移除 GUI 对象之前让 Ruby 对象终止所有的关联处理,然后在 dealloc 方法中把关联的 Ruby 对象也干掉。另一个方案是避免 Data_Wrap_Struct(), 改用 "软一点" 的方式去相互引用 (例如两者都位于各自所在环境的一个数组的相同 index 中). 如果让 Ruby 对象响应 GUI 事件,那么在 Ruby 处理事件前最好就把所有的可能用到的参数都准备好,Ruby 处理事件中要避免回访可能被移除的 GUI 对象,如果不能避免,就想办法 (例如锁) 告诉其他部分的 ObjC 程序:事件未理完,刀下请留人...

字符串的内部表示和转换

关于 Cocoa 字符串的内部表示,在 64 位 intel 上,NSUnicodeEncoding 对应 Ruby 的 UTF-16LE, 但字符串长度不考虑 surrogate pair, 而 Ruby 是考虑 surrogate pair 的。下面代码区别的原因就在于 "𝄞" 这个字符的 unicode 是大于 65535 的,在 utf-16 中必须用两个码点也就是 4 字节才能表示:

ruby:

"𝄞".encode('utf-16le').length //=> 1

objc:

@"𝄞".length //=> 2

双方共享字符串的时候,要注意这个区别。Cocoa 的 "character" 概念,其实是一般 unicode 文档中说的码点,而 Cocoa 中的 "glyph" 才对应平常我们说的 "字符". 如果要在 Cocoa 中计算字符串的字符数,一种办法是让 ruby 解决,一种办法是用 glyph 相关的 API (请参考 core text 相关文档), 还有一种办法是用 UTF32 (UTF32 一个码点能表示的字符范围有几十亿,现在和将来好长一段时间码点和字符都是一一对应的)

size_t len = [@"𝄞" lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4 //=> 1

嵌入 Ruby 2.0

后来尝试了嵌入 Ruby 2.0, 写法简化了一些:不用写 RUBY_GlOBAL_SETUP 了。但是载入 prelude 处理到 encdb 时会崩溃,然后连进入 debug 模式的 xcode 都会一起崩溃掉... 以后嵌入 API 可能也一并修改,估计 2.0.x 可以修正,应关注 这个 issue

P.S.

为什么不嵌 mruby: 因为我的应用需要 Fiber 但 mruby 还不支持. 为什么不嵌 Xcode 带的 Ruby Framework: 因为那是 1.8 的. 为什么不嵌 MacRuby: 因为嵌入的目的是发挥 Ruby 的长处 (字符串,标准库,gem...), 用 Ruby 写 GUI 的话巨长的方法名会让你抓狂的。

嗯 好文章。 刚看标题时,我还在想为什么不用 mruby, 看完后了解到 你原来需要 Fiber . mruby 现在还没有 fiber, @matz 说过把 fiber 加入到 mruby 是很不错的,只是他现在没时间做这件事。

是不是搞进去以后,某些 Gem 的功能就可以拿来用了?比如 Nokogiri 的东西

@huacnlee 是的,我就用了一个 fuzzy_file_finder 的 gem, 还有 yaml, http 什么的处理都容易多了

补充了一点字符串和 Ruby 2.0 相关的内容...

需要 登录 后方可回复, 如果你还没有账号请 注册新账号