2009-11-25 22:33:53 阅读(0) 评论(0)
2009-11-15 22:28:49 阅读(170) 评论(6)
。但这个做法等于是自己定义一种转义规则了。
当一个项目构建过程比较复杂的时候,可以考虑用通用语言来管理构建过程。对于通用语言来说,处理字符串转义、算术运算、循环遍历等事情易如反掌,无需使用 make 那些神奇而又晦涩的神技。而跨平台的通用语言的文件系统库往往也比 shell 有更好的移植性。至于子模块依赖问题,对于支持 closure 、 coroutine 的语言来说,很容易就能实现依赖关系的惰性计算,只需要生成整个依赖图中于构建目标相关的那部分,速度就快多了。
我在做的一个项目的构建过程就十分复杂,需要把一些配置文件从一种格式转成另一种格式,然后再嵌入主程序中;用到的美术素材也需要转格式。这些过程需要用到好几种转换工具,有一些转换工具就直接要用 lua 编写。我原先是用 GNU Make 来做这件事情的,把各种神奇的特性都用上了,最后碰到空格文件名的时候,还是放弃了,决心自己用 lua 实现一套构建系统,我把它叫做 lua-make
因为编译器、转换工具都是命令行工具,所以这个构建系统碰到的第一个问题是要能启动进程。lua 标准库中启动进程有两个函数,io.popen 和 os.execute 。其中 io.popen 不支持 Windows。而 os.execute 要依赖于操作系统的 shell ,移植性不佳比如,在 bash 中,我们可以写这样的代码:
如果要用 lua 来启动,就是
可是上述代码在 Windows 中就跑不通了。这是因为 Windows 的 shell 是 cmd ,语法不同,字符串转义规则不同,目录分隔符也不同。
归根到底, os.execute 是不可移植的。所以我用了 Lua-Ex 。Lua-Ex 提供了创建管道的函数(ex.pipe),启动进程并重定向标准输入输出的函数(ex.spawn),刚好适合做构建系统。不过,pipe 和 spawn 是比较底层的函数,要想简单的完成一串管道的命令,还需要一些语法糖。
我做了一个 path 模块和一个 shell 模块来做实现这个语法糖。先前的 bash 表达式,用我提供的语法糖来写的话就是这样:
上述表达式中,运算符/
用来重定向标准输出,相当于 bash 中的|
、<
或>
;运算符%
用来重定向标准错误,相当于 bash 中的2>
。
path["out/log"] 是一个路径对象。我重载了路径对象metatable中的 __div ,所以也可写成 path["out"] / path["log"] 或者 path.out / path.log 又或者 path.out / "log" 。如果用 setfenv 把当前执行环境设成 path ,甚至可以直接写成 out/log 。
路径的比较也十分简单,我能保证同一个路径在内存中只有一份,所以 path["x/b"] 、 path["x"] / b 、 path["x/a/../b"] 、 path["x\\b"] 都是同一个对象。
我也重载了路径对象metatable中的 __call ,当它被调用的时候就会被当成可执行文件来执行。这就是为什么上述代码可以直接写path["bar"]('yyyyy', 'aaa') 的缘故。
对于一个构建系统来说,核心部分是目标的依赖关系管理。这方面,我主要是造搬 GNU Make 的模型,只是语法用 lua 的表来描述。
这里我碰到一个问题,就是多任务的并行执行。make 有一个 -j 选项,可以并行执行多个任务。用 lua 怎么实现类似的功能呢?用 ex.spawn 可以创建进程,用 ex.wait 可以等待一个进程退出,但是如果同时启动了多个进程,怎样才能等待其中任意一个退出呢?Windows 中有一个 WaitForMultiObjects 可以做这件事情,但是一方面这不可移植,另一方面我也不想为了这一个功能而多写一个 lua C库。我用的办法是在主进程调用 ex.pipe 创建一个 pipe,为需要监视的进程创建一个辅助进程来监视,辅助进程等待被监视的进程退出,退出时向 pipe 发一个消息。所以主进程之需要读这个 pipe 就可以知道任意一个进程退出了。我把这个监视模块叫做 shell.watchdog 。
最终,要使用这个构建系统,大概是这样来写Makefile:
make 函数的第一个参数指最多允许多少个并行任务的意思,这里传 10,就相当于GNU Make 的 -j 10
make.filetarget 是说这是一个文件目标,而不是一个伪目标(即 GNU Make 的 .PHONY)。文件目标和伪目标相比,要增加判断文件修改时间、自动创建父级目录等功能。在我实现的构建系统中,一个目标默认是没有这些功能的,如果需要这些功能就得写 make.filetarget {......}
这里的watchdog +
是一个语法糖,意思是要把接下来的这一段 shell 表达式加入到 watchdog 这个监视对象中去监视。
大体上我就是这样实现 Lua 构建系统的,欢迎回帖讨论
2009-10-16 9:14:44 阅读(12) 评论(0)
用ActionScript的时间长了,实在是不能容忍里面还有我不认识的API。昨天终于搞清楚trackAsMenu的用处了,于是AS里面我不认识的API又少了一个。
在Flash创作工具中文版的界面上,这个trackAsMenu被翻译为“音轨作为菜单”,看到这几个字,我眼睛湿润了。trackAsMenu的意思应该是“以菜单的方式追踪鼠标”。这个选项在ActionScript3里面是SimpleButton和MovieClip上的属性,如果打开这个选项,有两个效果:
所以,这个功能用来制作菜单时会有用。只要把各个菜单项都设上trackAsMenu属性,原先按下鼠标时的那个对象以外的对象也可以触发click事件了。
2009-10-15 21:37:21 阅读(9) 评论(0)
Windows命令行参数的转义分为两个步骤,第一个步骤是cmd解析你输入的字符串决定要启动哪些进程,进程之间是否要标准输入输出重定向等等;第二个步骤是CRT把传入进程的命令行字符串切成多个参数,填好argc和argv传入main。
如果一个程序不是被cmd启动的,而是通过快捷方式或者bash等别的shell启动的,或者干脆直接调用API CreateProcess,那么第一个步骤可以无视;如果一个程序并不使用argc、argv,而自行调用GetCommandLine等Windows API,则第二个步骤可以无视。
第一个步骤中,| ^ " &
等字符是特殊字符。如果特殊字符出现在双引号外,需要被转义。但如果出现在双引号中,就不需要也不能被转义。转义时,^^
代表^
,^|
代表|
,^"
代表"
等等
第二个步骤的具体实现取决于CRT,在Windows XP中用得最广泛的CRT是msvcrt.dll。Windows XP附带的系统程序大都使用了它,用VC6或mingw的gcc编译的程序也使用这个CRT。我看了一下VC6附带的源码,切参数这件事情的规则相当复杂。
首先是空格和tab如果不在双引号,就会被当作参数的分隔符,而双引号引住的空格和tab代表空格tab字符本身。
其次是连写双引号的特殊处理。如果想要把双引号作为参数内的字符,那么至少要连写两个双引号:""
。当他遇见连续的两个双引号时,他会把其中一个双引号作为双引号字符,另一个双引号作为引用范围的开始或结束。也就是说,如果你已经在引号中了,连写2个双引号的话,就会产生一个"
字符,并且关闭引用。要想产生一个"
字符但不改变引用状态,就需要连写三个双引号"""
。
最变态的是用反斜杠(即\
)转义的规则。对于n个反斜杠外加一个双引号会被转义。如果n为奇数,那么会被转义成(n-1)/2个\
字符外加一个"
字符。如果n为偶数,那么会被转义成n/2个\
字符,然后再进入或结束引用范围。而如果反斜杠后面并不跟随双引号,反斜杠就不转义。
值得注意的是,因为cmd和CRT对双引号字符的转义规则并不一致,所以cmd和CRT对于某个字符是否被引用的判断也是不一致的。比如说,如果你想进行一次Subversion提交,注释中写上
,那你得写:hello " | " | " | " world
svn commit -m "hello """ ^| """ | """ ^| """ world"注意,其中某些
|必须要转义,而另一些则必须不转义。上面说的还仅仅是msvcrt.dll切参数的规则,这个规则和cygwin的CRT是不一致的,也就是说,如果你在cmd中要调用cygwin编译的程序,那么转义规则又不一样。幸好这种情况并不是很多,用cygwin的话,就用bash好了,至少全世界的bash解析参数的方式都一样。
2009-9-2 12:56:01 阅读(18) 评论(0)
第一个原因是如果新写的代码改用haXe,新写的代码依赖于部分AS代码,而另一部分AS代码又会依赖于haXe,那么就会导致循环依赖。虽然有办法可以解决,但是每种解决方案都有代价,要么是把现有AS代码拆成两部分来编译,要么是用ApplicationDomain.getDefinition之类的字符串解耦合办法。这些办法都不是我想要的。
第二个原因在于haXe用的全局变量flash.Lib.current. 把一段haXe代码编译成SWC或者SWF,分别作为静态库和动态库使用时,会有微妙的差异(参见http://haxe.org/doc/advanced/swc)。而我如果想要在开发期间缩短编译时间,必须要把haXe部分编译成动态链接的SWF。这个问题体现了“全局变量的邪恶性”。全局变量之所以邪恶在于其“全局”概念的不可控,在模块角度看是全局的东西,到了装载器的角度来看就有时候是而有时候不是了,就会导致行为不一致。
2009-8-30 21:44:27 阅读(37) 评论(0)
主流的flash开发工具当然是Flex Builder/Flex SDK/Flash创作工具。一般用Flash创作工具生成素材,用Flex SDK来编译代码,喜欢IDE环境的就用用Flex Builder。但除此之外,还有一些非主流的开发工具选择。
现在能生成flash的编译器已经有很多了,真的不必拘泥于龟一般的mxmlc. 前一篇文章,我介绍了haXe。除此之外,另一个选择是swftools中的as3compile,这个编译器号称是要兼容mxmlc,不过就我目前测试的情况来看,它有一个缺陷,即不支持闭包中访问upvalue。
当然我不认为mxmlc慢是因为它用了Java,javac也是Java写的,快得很呢。
由于AVM开源了,还可以用AVM开源后提供的编译器,AVM开源后的项目名叫做Tamarin,源码在这里下载。这是一个C++编写的编译器,现在想尝尝鲜的话,可以找到Tamarin里面的utils/avmc,里面就是这个编译器。不过avmc编译出来的目标文件是.abc,还需要包装上swf的文件头才能用,这件事情可以用前面提到的as3compile来做。比较头疼的问题在于如何引用flashplayer的API,这些API在flex_sdk_3/frameworks/libs/player/playerglobal.swc里面是有,要把这个playerglobal.swc导出成.abc我没试过,但应该不难。
以上是一些编译器,至于素材的制作,可以考虑swftools中swfc。和官方的Flash创作工具相比,它的优势在于源文件.sc格式是一个文本格式,工具又是命令行工具,因此易于用脚本生成代码。缺点有两个,首先是缺乏“所见即所得”编辑器,其次是一些功能缺失,比如向ActionScript导出元件。用swfc来打包素材已经足用了,我就用swfc来把一些公司自定格式的动画素材批量转换成swf格式。
2009-8-30 21:13:40 阅读(54) 评论(0)
其实我早就听说过haXe。那是大概两年前,mtasc停止开发,转而开发haXe. 当时我没怎么注意,直到现在对mxmlc的编译速度忍无可忍的时候,才偶然发现改用haXe也许是个解决办法。
haXe主页上强调这是一种“multiplatform language”的语言,可以编译成JavaS
从haXe的语言特性来看,这是一种大路货的面向对象语言,跟Java、C#、ActionScript3都差不多。对我而言,haXe比ActionScript3好的地方有:
haXe比ActionScript3差的地方有:
从ActionScript3迁移到haXe时还需要考虑的事情包括:
2009-8-14 11:31:53 阅读(49) 评论(0)
2009-8-12 9:45:50 阅读(11) 评论(0)
对数据结构或接口来说,如果能轻易地分辨出哪些数据不会改变,哪些数据可能会改变,以及谁可能改变这些数据,就会非常有用。以上可借助语言的类型系统做到。数据可以被标记为const或immutable,而其默认是可变的(即mutable ) 。
不 可改变的数据可以用immutable表示。immutable数据构建之后,在程序整个运行期间都会保持原值。immutable数据可以放在 ROM(只读存储器)中或者由硬件设为只读的内存中。由于immutable数据不会改变,所以可以得到许多程序优化的机会,还可便于采用函数式编程。
const适用于不能通过引用本身改变数据的引用。不过,这块数据还时可能被其他引用所改变。若向接口传入数据时需要保证该接口中不修改该数据,则应采用const。
immutable和const都是transtive(具有可传递性),这意味着通过immutable引用取得的数据同样也是immutable的,const亦是如此。
immutable最简单的用法是用作存储类。可以用它来声明明确的常量。
immutable int x = 3; // 将x设为3
x = 4; // 错误,x不可改变
char[x] s; // s 是一个包含三个char的数组
数据类型也可以由初始化表达式推断:
immutable y = 4; // y的类型是int
y = 5; // 错误,y不可改变
如果不写初始化表达式,相应的构造函数也可以初始化immutable数据:
immutable int z;
void test()
{
z = 3; // 错误,z不可改变
}
static this()
{
z = 3; // 正确,没有静态初始化表达式的immutable数据可以赋值
}
除了函数类的immutable数据外,其他immutable声明的初始化表达式都必须能在编译时求值:
int foo(int f) { return f * 3; }
int i = 5;
immutable x = 3 * 4; // 正确,12
immutable y = i + 1; // 错误,无法在编译时求值
immutable z = foo(2) + 1; // 正确,foo(2)能在编译时求得值——7
非静态的局部immutable数据的初始化表达式是在运行时计算的:
int foo(int f)
{
immutable x = f + 1; // 运行时求值
x = 3; // 错误,x不可改变
}
由于immutable数据具有可传递性,由immutable数据引用的数据也是不变的:
immutable char[] s = "foo";
s[0] = 'a'; // 错误,s指向一块不可改变的数据
s = "bar"; // 错误,s不可改变
immutable声明可以作为左值出现,比如它们的地址都是可以获取到的,再如它们会占据存储空间。
除了以下区别之外,以const声明的数据和immutable的完全相同:
一定不会被改变的数据可以标记为immutable。immutable关键字可以被用作type constructor:
immutable(char)[] s = "hello";
括号中的类型被标记为immutable。因此,一方面,s能被设为新的值,另一方面,s[]的内容却不能被改变:
s[0] = 'b'; // 错误,s[]不可改变
s = null; // 正确,s自身并不是immutable的
immutable具有可传递性,这意味着被immutable类型引用的数据也是immutable的:
immutable(char*)** p = ...;
p = ...; // 正确,p可以改变
*p = ...; // 正确,*p可以改变
**p = ...; // 错误,**p不可改变
***p = ...; // 错误,***p不可改变
把immutable用作存储类相当于把immutable用作整个声明类型的type constructor:
immutable int x = 3; // x的类型是immutable(int)
immutable(int) y = 3; // y不可改变
第一种方式:使用本来就不变的字面量(literal),比如字符串字面量。字符串字面量始终都是immutable的。
auto s = "hello"; // s的类型是immutable(char)[5]
char[] p = "world"; // 错误,immutable类型不能隐式转换为非immutable类型。
第二种方式是把数据转换为immutable。这样做时,需要由程序员确保不存在其他的对该块数据的可变引用。
char[] s = ...;
immutable(char)[] p = cast(immutable)s; // 未定义行为
immutable(char)[] p = cast(immutable)s.dup; // 正确,只存在一个引用
使用.idup属性,可以便利的创建某个数组的不变副本。
auto p = s.idup;
p[0] = ...; // 错误,p[]不可改变
immutable类型可通过强制转换而被移除。
immutable int* p = ...;
int* q = cast(int*)p;
不过,这并不意味着可以修改这块数据。
*q = 3; // 编译通过,但结果会导致未定义行为
在有些情况下,强制抹除不变性是必要的。比如涉及某个不能更改的库,而库中的静态类型并不正确。有史以来,强制转换一直很直接很有效。使用强制转换抹除不变性时,必须要承担保证数据不变的责任,因为它已经不再由编译器静态保证了。
immutable成员函数表示this以及被this引用的任何数据都是不变的。可以这样声明:
struct S
{ int x;
immutable void foo()
{
x = 4; // 错误,x不可改变
this.x = 4; // 错误,x不可改变
}
函数的const和immutable属性也可以写在参数列表的右括号之后:
struct S
{
void bar() immutable
{
}
}
const类型和immutable类型类似,唯一的区别在于const建立的是数据的只读视图。数据本身随时可能被其他引用了该数据的变量所修改。
在const成员函数中,不允许通过该函数的this指针改变对象任何部分的数据。
immutable和非immutable类型都可以隐式转换为const类型。非immutable类型不能隐式转换为immutable类型,反之亦然。
| 特性 | D | C++98 |
|---|---|---|
| const关键字 | 有 | 有 |
| immutable关键字 | 有 | 无 |
| const表示方式 | 函数式://指向指向const int的const指针的指针 | 后缀://指向指向const int的const指针的指针 |
| 有传递性的const | 有://指向指向const int的const指针的const指针 | 无://指向指向int的指针的const指针 |
| 强制抹除const | 有:// 指向const int的指针 | 有:// 指向const int的指针 |
| 强制抹除const后进行修改 | 无:// 指向const int的指针 | 有:// 指向const int的指针 |
| 以顶级const区分重载的函数 | 有:void foo(int x); | 无:void foo(int x); |
| 变量的const别名 | 有:void foo(const int* x, int* y) | 有:void foo(const int* x, int* y) |
| 变量的immutable别名 | 无:void foo(immutable int* x, int* y) | 没有immutable |
| 字符串字面量的类型 | immutable(char)[] | const char* |
| 将字符串字面量转换为非常量 | 不允许 | 允许但不推荐 |
2009-8-5 16:06:23 阅读(46) 评论(0)
#!/bin/bash
(
find . -iname '*.TGA' -exec echo '
(let ((image (car (file-tga-load 1 "{}" "{}"))))
(file-png-save-defaults 1 image (car (gimp-image-get-active-drawable image)) "{}.png" "{}.png")
(gimp-image-delete image)
)
' \;
echo '(gimp-quit 1)'
) | gimp-console -d -i -b -