现在如何在 Mac OS X 中写一套输入法 (一)

zonble 曾经有一篇系列文章 (1, 2, 3),比较简略的介绍了如何在 Mac OS X 下如何开发一个输入法,其中有一部分解释不足的地方,我希望在本文补足,如果你还没有读过 zonble 的原文,请先读读。

第一部分,关于资源文件。

我们都知道,Resource Fork 是 Mac OS Classic 时代的残余,通过 Resource 来作程序的定制 (比如 l10n) 也是在 Classic 时代推荐的,在 Modern Carbon 与 Cocoa 已经被 .nib 文件替代了,然而,Mac OS 的系统 Component 的编写直到现在仍然未有变化,仍然需要 Component 程序有一个 Resource (.rsrc) 文件,Component Manager 利用这个 Resource 文件了解 Component 的名字、入口点和图标等信息。

然而在现在的 Mac OS X 中的几个改进值得注意:

首先,TN2012 指出,虽然原本 Component Manager 支持你将 Component 编译为一个 .dylib,将 .rsrc 作为它的 resource fork 使用,但对于 Mach-O Component 这个方法已经不推荐了,建议以 Bundle 的形式来存放,也就是将 .rsrc 文件存放在 .component/Contents/Resources/ 目录下。

顺便说一句,也许有人会把 Components 和 Bundles 的概念混淆,其实两者的差异是很大的,Component 是系统启动/用户重新 Login 时就会载入的代码,放在 /Library/Components (或者 /System/Library/Components 和 ~/Library/Components) 下的 .component 文件都会被尝试载入。如果你不 Logout/Login,那这些代码不会被重新载入。

Bundle 只不过是一种用目录来组织文件的形式,通过放置 PkgInfo 和 Info.plist 文件,使系统从外在把这个目录当作整个一个文件来处理。

所以 Component 和 Bundle 其实是两个正交的概念:Component 不一定要用 Bundle 来存储,Bundle 存储的也不一定是 Component (有可能是 .app, .framework)。

其次,TN2012 同样提到,Mach-O 格式的 Component 是唯一支持 Intel 架构的二进制格式,老旧的 CFM 格式 Component 已经不应该再使用。所以我们应该修改 .rsrc 的源格式 .r 文件,使之支持 Intel 架构。

在介绍怎么修改之前,先扯开一点,说说 .r 和 .rsrc。和 Windows 一样,资源文件是要编译以后才能使用的,源文件格式称为 Rez Input Format,而目标文件格式显然称为 Rez Output Format。用来编译和反编译的分别是 /Developer/Tools 目录下的 Rez 和 DeRez 两个命令行程序。

显然,我们需要关心的只是源格式。然而这个源格式非常的灵活,因为不只是 Component 要用到 resource,其他的东西也要用到,比如应用程序的 menu 信息,就曾经是用 resource 来指定的 (和 Win32 编程一致)。

资源有两个需要注意的概念,分别是 Resource Type 和 Resource ID。在 .r 文件中,我们通常这样写:

resource 'abcd' (123)
{
    ... 资源的内容
}

resource 'efgh' (456)
{
    ... 资源的内容
}

其中 'abcd''efgh' 就是 Resource Type,通常这四个字符是 Apple 预定义的,虽然也可以随便起,但 Component Manager 只会寻找它所认识的资源类型:'thng' (是 thing 的缩写,不知道为什么起这个名字)。而 123 和 456 是 Resource ID,其实只要你在这个 .r 里定义的同一个 Resource Type 的 Resource ID 不重复就行了,所以不同类型的资源其实可以用同一个 ID,后面我们会看到,BIM.r 就是如此的。

而中间省略的部分,资源的内容,根据不同的 Resource Type 而定,在 TN1004 中,给出了 'thng' 的内容格式。我们会在下面逐一解释。

关于 Resource 的资料,可以参考 Inside Macintosh 的 Resource Manager 部分。

下面是我们解释 BasicInputMethod 的资源文件 BIM.r:

#define UseExtendedThingResource 1
#include <Carbon/Carbon.r>
#include "BIMScript.h"

首先,必须在包含 Carbon 的头文件之前定义 UseExtendedThingResource,否则,Component Manager 就不支持扩展的 thng 格式,这是 m68k 到 Power PC 转换时出现的变化,在 Inside Macintosh 的 Component Manager 一章里没有介绍,但 TN1004 里解释了。包含 BIMScript.h 这个文件是为了引用一些常量 (资源文件可以引用头文件,这一点和 Win32 也是一样的)。

resource 'thng' (128)
{
    'tsvc',
    'inpm',
    'appl',
    0x8000 + kBIMScript * 0x100 +
    kBIMLanguage,

到了 thng 部分,Resource ID 128 其实是随便起的。前四个项目 zonble 的文章都解释了,注意在 TN2128 还介绍了如何创建 Palette 类型的输入法的方法,第一步就是要将这里的第二项 'inpm' 改为 'cplt'。而第四项,Component 的 Flags 是你的 International 设置里看到的所有输入法时右侧的 Script 一栏的基础。需要注意,如果你希望写一个通用的输入法框架 (类似 SCIM 或 OpenVanilla),像下面这样在 BIMScript.h 里定义常量会比较合适:

#define kBIMScript      smUnicodeScript
#define kBIMLanguage    langUnspecified

其中 script 的定义可以让你在 International 设置里看到 Script 是 Unicode,language 的定义的用途我还不太清楚,欢迎补充。

关于可以定义的 script 和 language 常量,可以看 Script Manager 的参考手册,它的 API 虽然几乎全部 deprecated 了,但定义的常量还是有用的。

    kAnyComponentFlagsMask,
    'dlle', kBaseResourceID,
    'STR ', kBaseResourceID,
    'STR ', kBaseResourceID + 1,
    'ICON', kBaseResourceID,

下面定义了几个重要的参数:Component 的入口点函数应该在 'dlle' 资源里找,Component 的名称字符串,应该在 ID 为 kBaseResourceID 的 'STR ' 资源里找,Component 的信息字符串,必须在 kBaseResourceID + 1 的 'STR ' 资源里找,如果你看完了上面关于资源的解释,这就应该很好理解了。让我们看看实际这几个资源的定义:

resource 'dlle' (kBaseResourceID) {
    "BIMComponentDispatch"
};

resource 'STR ' (kBaseResourceID)
{
    "Basic Input Method"
};

resource 'STR ' (kBaseResourceID + 1)
{
    "A Really Cool Input Method"
};

就一目了然了,入口点是一个叫 BIMComponentDispatch 的函数,名称是 "Basic Input Method",信息是 "A Really Cool Input Method"

回来继续看 ‘thng’ 资源的内容:

#if UseExtendedThingResource
    0x00010000,
    componentHasMultiplePlatforms,
    kBaseResourceID
    {
        0x8000 + kBIMScript * 0x100 +
        kBIMLanguage,
        'dlle',
        kBaseResourceID,
        platformIA32NativeEntryPoint,
    }
#endif

首先,我们注意到,这部分是 Power PC 开始扩展的内容,所以应当用条件编译括起来,然后是这个 Component 的版本,这里是 1.0。第 3 项是 Icon Family 的资源 ID,什么是 Icon Family 呢?在 OS Classic 里出现的这个概念是指,你可以把一堆 Icon 组织为一个 Family,分别是不同大小、色深的图标,当载入资源时,你只需要指定载入哪个 Family 的 ICON,就可以自动根据你要用到 Icon 的位置,判断出应该用这个 Family 里的具体哪个图标了。Basic Input Method 的 Icon 原来真的是在这里定义的,不过正如 zonble 原文提到,我们应该只能看到 'kcs8' 这个图标了,这是一个 32×32 的 256 色图标。

不过,现在我们不必使用 zonble 给出的修改资源文件的方法来修改图标了,TN2128 中解释,通过资源文件来设置图标的方法已经不建议再用,你可以通过在 Component 的 Info.plist 中指定 tsInputMethodIconFileKey 一项来定义图标。你可以参考 QIM 的作法,修改 Xcode 项目的 Products/BasicInputMethod.component/Contents/Info.plist 文件,添加下面几句:

<key>CFBundleIconFile</key>
<string>Icon.icns</string>
<key>tsInputMethodIconFileKey</key>
<string>Icon.icns</string>

其中 Icon.icns 就是 .component/Contents/Resources 下放置的一个图标文件,支持 .tiff 和 .icns。

接下来的 { } 中部分是一个专门描述模块的 Platform Info 的部分,其 4 项依次是 Component Flags,’dlle’ 和 ‘dlle’ 资源的 ID,最后一部分最重要,描述编译在哪个平台下执行,这里指定 IA32,就是只给 Intel Macs 用,你也可以参照 TN2012 来编译 Universal Binary 的 Resource。

至此,资源文件的介绍就到这里。使用官方下载的 BasicInputMethod 的项目加上我这里附上的 BIM.r,应当可以编译出一个放在 /Library/Components/ 下便能顺利执行的输入法模块来,希望这篇介绍能帮助更多的朋友开发开放源代码的 OS X 输入法。

找到一个不错的 Linux 发行版

最近因为开发内核程序的原因,需要找一个很小的发行版,放在 qemu 虚拟机里测试。

找了找,发现 Core GNU/Linux 很不错,光盘启动安装,把硬盘挂载上,分区,执行安装脚本,chroot,装上 grub 就搞定了,总共也只用 200M 左右的空间,主要软件都是 bleeding-edge 的,很方便。

管理大型的 Cocoa 项目

NetNewsWire 的作者 Brent Simmons 最近的一篇文章介绍了如何有效的管理大型 Cocoa 项目。他的建议可以总结为下面几条:

为了改善代码的可读,可查找性,应该遵循:

  1. 只对那些没有明显关系的对象之间的交互使用 Notification。
  2. Key-Value Observing 也是很危险的,应该只对 Preferences 项目使用这一特性。
  3. 只将 Binding 用于很简单的情形,复杂的 TableView 还是用 datasource/delegate 比较好。

管理代码时可以使用的技巧:

  1. #pragma mark 来划分代码的区域
  2. 用 Ctrl-2 来列出当前打开文件的符号 outline。
  3. 用 Shift-cmd-D 来快速打开指定文件。
  4. 用 opt-cmd-T 来将当前打开的文件和左侧的目录树同步。
  5. ctrl+double click 打开符号的定义,opt+double click 打开符号的文档。
  6. 在文件系统中用平面方式组织文件,不划分多层目录,在 Xcode project 中用 group 来划分层级结构。

对于有 Cocoa 开发经验的人,尤其是管理过这种超过 200 个源文件的较大项目的人来说,这些经验是很有用的。