4.4 其他修改

接下来要介绍的是crowbar ver.0.2中除了GC之外的其他修改的地方。

4.4.1 修改语法

对语法作了如下修改。

数组——表达式之后可以加 [表达式]。

函数调用——表达式后面可以加函数名(参数列表)。

自增/自减——表达式后面可以加 ++或 —。

增加了很多可以追加在表达式后面的语法。在语法结构上引入了非终结符 postfix_expression(这个名字是从K&R [2] 的附录C中得来的)。

unary_expression

: postfix_expression

| SUB unary_expression

;

postfix_expression

: primary_expression

/ 引用数组元素 LB,RB是“[”和“]” /

| postfix_expression LB expression RB

/ 调用函数 /

| postfix_expression DOT IDENTIFIER LP argument_list RP

| postfix_expression DOT IDENTIFIER LP RP

/ 自增,自减 /

| postfix_expression INCREMENT

| postfix_expression DECREMENT

;

句柄LB 和RB 是Left.Bracket 和Right.Bracket 的简写,分别代表“ [”和“ ]”。

根据这个语法结构创建的结构体如下所示(crowbar.h)。因为都是表达式,所以都加入到了 Expression结构体的共用体中。

/ 引用数组元素 /

typedef struct {

Expression *array;

Expression *index;

} IndexExpression;

/ 自增/自减 /

typedef struct {

Expression *operand;

} IncrementOrDecrement;

/ 调用函数 /

typedef struct {

Expression  *expression;

char   *identifier;

ArgumentList *argument;

} MethodCallExpression;

4.4.2 函数的模拟

crowbar ver.0.2的数组配备了下面这些“像函数一样的函数”。

给数组增加元素

a.add(3);

取得数组的大小

size = a.size();

改变数组的大小

a.resize(10);

另外,(顺便)也给字符串添加了函数。

取得字符串的长度

len = "abc".length();

之所以说起函数的“模拟”,是因为现在在crowbar中还没有为类型(或者对象)分配函数的通用方法。book_ver.0.2的实现虽说属于偷工减料,但也算是用了嵌入代码的方式,处理得还不错。(截取自eval.c,见代码请单4-6。)

代码清单4-6 eval_method_call_expression()

static void

eval_method_call_expression(CRB_Interpreter inter, CRB_LocalEnvironment env,

Expression *expr)

{

CRB_Value *left;

CRB_Value result;

CRB_Boolean error_flag = CRB_FALSE;

eval_expression(inter, env, expr->u.method_call_expression.expression);

left = peek_stack(inter, 0);

if (left->type == CRB_ARRAY_VALUE) {

if (!strcmp(expr->u.method_call_expression.identifier, "add")) {

CRB_Value *add;

check_method_argument_count(expr->line_number,

expr->u.method_call_expression

.argument, 1);

eval_expression(inter, env,

expr->u.method_call_expression.argument

->expression);

add = peek_stack(inter, 0);

crb_array_add(inter, left->u.object, *add);

pop_value(inter);

result.type = CRB_NULL_VALUE;

} else if (!strcmp(expr->u.method_call_expression.identifier,

"size")) {

check_method_argument_count(expr->line_number,

expr->u.method_call_expression

.argument, 0);

result.type = CRB_INT_VALUE;

result.u.int_value = left->u.object->u.array.size;

} else if (!strcmp(expr->u.method_call_expression.identifier,

"resize")) {

CRB_Value new_size;

check_method_argument_count(expr->line_number,

expr->u.method_call_expression

.argument, 1);

eval_expression(inter, env,

expr->u.method_call_expression.argument

->expression);

new_size = pop_value(inter);

if (new_size.type != CRB_INT_VALUE) {

crb_runtime_error(expr->line_number,

ARRAY_RESIZE_ARGUMENT_ERR,

MESSAGE_ARGUMENT_END);

}

crb_array_resize(inter, left->u.object, new_size.u.int_value);

result.type = CRB_NULL_VALUE;

} else {

error_flag = CRB_TRUE;

}

} else if (left->type == CRB_STRING_VALUE) {

if (!strcmp(expr->u.method_call_expression.identifier, "length")) {

check_method_argument_count(expr->line_number,

expr->u.method_call_expression

.argument, 0);

result.type = CRB_INT_VALUE;

result.u.int_value = strlen(left->u.object->u.string.string);

} else {

error_flag = CRB_TRUE;

}

} else {

error_flag = CRB_TRUE;

}

if(error_flag) {

crb_runtime_error(expr->line_number, NO_SUCH_METHOD_ERR,

STRING_MESSAGE_ARGUMENT, "method_name",

expr->u,method_call_expression.identifier,

MESSAGE_ARGUMENT_END);

}

pop_value(inter);

push_value(inter, &result);

}

说点题外话,最初在获取数组元素数的时候使用的是 get_size(),作为一个getter函数,为了统一性着想,名字理所当然是 get_xxx()。但是,考虑到这个函数使用频繁,并且经常要放在 for语句中使用,函数的名字如果太长使用起来不太方便,因此还是取名为 size()了。

我常常在想,虽然统一性很重要,但是方便性一样重要。不论是制作语言还是程序库,要想兼顾这两个方面还真是不容易。

不过,取得数组大小为什么不是只用 length就可以了,而要用函数呢?取字符串长度时用的是 length(),而 Vector和 ArrayList这样取大小的为什么用的是 size()呢?我想这只是为了兼容之前的内容而产生的不统一。

4.4.3 左值的处理

在一般的表达式中,一个变量表示该变量储存的值。比如,将5 赋值给 a,当表达式中的 a 替换为5 时表达式的结果保持不变。但是,变量在赋值的左边时,此时将

a = 10;

变为

5 = 10;

是行不通的。总之,变量在赋值语句的左边时,变量代表的不是它所储存的值,而是储存值的地方(即变量的内存地址),我们称其为左值(left value)。

book_ver.0.1中,赋值语句的左边只能放变量。赋值语句的语法规则如下所示:

expression

: IDENTIFIER ASSIGN expression

;

但是在引入了数组之后,赋值语句的左边就可以写稍微复杂一点的表达式了。

给二维数组a赋值,其中一个下标为以b[i]为参数

调用函数func()后的返回值

a[i][func(b[i])] = 5;

当然,使用老的语法结构不能对应这种情况,于是,我们将语法结构改写成下面这样。

expression

: postfix_expression ASSIGN expression

;

左边变成了 postfix_expression,而现在能成为赋值语句的对象的,只有 primary_expression(变量名)和 postfix_expression(数组元素)了。

接着,在eval.c 中首先计算右边的值,然后调用 get_lvalue() 函数取得左边表达式的地址,详见代码清单4-7。并且,这里调用的 peek_stack() 函数会在不清除栈的情况下获取值,这样一来右边的值会作为赋值表达式整体的值残留在栈中,也是件好事。

代码清单4-7 eval_assign_expression

static void

eval_assign_expression(CRB_Interpreter inter, CRB_LocalEnvironment env,

Expression left, Expression expression)

{

CRB_Value *src;

CRB_Value *dest;

/ 首先计算右边值 /

eval_expression(inter, env, expression);

src = peek_stack(inter, 0);

/ 取得左边的地址 /

dest = get_lvalue(inter, env, left);

dest = src;

}

那么, eval_assign_expression()中调用的函数 get_lvalue()又是什么样子呢?请见代码清单4-8。将标识符和数组分开进行处理,如果是数组的话,可以利用 get_array_element_lvalue()(见代码清单4-9)函数返回数组元素对应的地址。

代码清单4-8 get_lvalue()

CRB_Value *

get_lvalue(CRB_Interpreter inter, CRB_LocalEnvironment env,

Expression *expr)

{

CRB_Value *dest;

if (expr->type == IDENTIFIER_EXPRESSION) {

dest = get_identifier_lvalue(inter, env, expr->u.identifier);

} else if (expr->type == INDEX_EXPRESSION) {

dest = get_array_element_lvalue(inter, env, expr);

} else {

crb_runtime_error(expr->line_number, NOT_LVALUE_ERR,

MESSAGE_ARGUMENT_END);

}

return dest;

}

代码清单4-9 get_array_element_lvalue()

{

CRB_Value array;

CRB_Value index;

/ 运算[]左边的表达式 /

eval_expression(inter, env, expr->u.index_expression.array);

/ 运算[]中的表达式 /

eval_expression(inter, env, expr->u.index_expression.index);

/ 取得两个变量的值 /

index = pop_value(inter);

array = pop_value(inter);

/ 检查数据类型 /

if (array.type != CRB_ARRAY_VALUE) {

crb_runtime_error(expr->line_number, INDEX_OPERAND_NOT_ARRAY_ERR,

MESSAGE_ARGUMENT_END);

}

if (index.type != CRB_INT_VALUE) {

crb_runtime_error(expr->line_number, INDEX_OPERAND_NOT_INT_ERR,

MESSAGE_ARGUMENT_END);

}

/ 检查下标范围 /

if (index.u.int_value < 0

|| index.u.int_value >= array.u.objext->u.array.size) {

crb_runtime_error(expr->line_number, ARRAY_INDEX_OUT_OF_BOUNDS_ERR,

INT_MESSAGE_ARGUMENT,

"size", array.u.object->u.array.size,

INT_MESSAGE_ARGUMENT, “index”, index.u.int_value,

MESSAGE_ARGUMENT_END);

}

/ 返回地址 /

return &array.u.object->u.array.array[index.u.int_value];

}

另外,在没有左值的情况下,取得数组元素值的引用时调用的函数 eval_index_expression(),它的内部也使用了 get_array_element_lvalue()。

static void

eval_index_expression(CRB_Interpreter *inter,

CRB_LocalEnvironment env, Expression expr)

{

CRB_Value *left;

left = get_array_element_lvalue(inter, env, expr);

push_value(inter, left);

}

自增/自减运算符也同样适用于 get_lvalue()取得目标变量的地址。

4.4.4 创建数组和原生函数的书写方法

前面已经说过,用原生函数 new_array()可以创建常量数组 {1, 2, 3}。该原生函数的定义请见代码清单4-10。

代码清单4-10 new_array()

/ 递归调用子例程 /

CRB_Value

new_array_sub(CRB_Interpreter inter, CRB_LocalEnvironment env,

int arg_count, CRB_Value *args, int arg_idx)

{

CRB_Value ret;

int size;

int i;

if(args[arg_idx]. type != CRB_INT_VALUE) {

crg_runtime_error(0, NEW_ARRAY_ARGUMENT_TYPE_ERR,

MESSAGE_ARGUMENT_END);

}

size = args[arg_idx]. u.int_value;

ret.type = CRB_ARRAY_VALUE;

ret.u.object = CRB_create_array(inter, env, size);

if (arg_idx == arg_count-1) {

for (i = 0; i < size; i++) {

ret.u.object->u.array.array[i]. type = CRB_NULL_VALUE;

}

} else {

for (i = 0; i < size; i++) {

ret.u.object->u.array.array[i]

= new_array_sub(inter, env, arg_count, args, arg_idx+1);

}

}

return ret;

}

/ 原生函数本体 /

CRB_Value

crb_nv_new_array_proc(CRB_Interpreter *interpreter,

CRB_LocalEnvironment *env,

int arg_count, CRB_Value *args)

{

CRB_Value value;

if (arg_count < 1) {

crb_runtime_error(0, ARGUMENT_TOO_FEW_ERR,

MESSAGE_ARGUMENT_END);

}

value = new_array_sub(interpreter, env, arg_count, args, 0);

return value;

}

首先,这次修改在原生函数的参数中增加了 CRB_LocalEnvironment。

上面代码中的处理只是递归地创建了数组的数组,数组所需内存空间的开辟工作由 CRB_create_array() 完成。另外,为原生函数增加了形式参数,从而使 CRB_LocalEnvironment 可以作为参数传递进去。那么, CRB_LocalEnvironment用途是什么呢?在创建多维数组的时候,会多次调用 new_array_sub()开辟内存空间。如果GC在进行上述操作的中途启动的话,就可能会立刻将刚分配好的数组释放掉。因此,为了把在函数中创建的对象标记为不回收的目标,我们需要把 CRB_LocalEnvironment传递到函数中。

具体来说, CRB_LocalEnvironment的成员 ref_in_native_method中保存了在原生函数中创建的对象(具体请参考4.2.4节的定义)。

类型 RefInNativeFunc 的定义如下,它以链表的形式保存着指向对象的数组。

typedef struct RefInNativeFunc_tag {

CRB_Object *object;

struct RefInNativeFunc_tag *next;

} RefInNativeFunc;

这种设计的问题在于,创建于原生函数内部的对象在函数结束前不能被释放。如果在原生函数中存在大量多次循环时,就会出现很多对象在创建后立即被丢弃的现象。有些对象在函数结束前不能被释放,由此造成了内存空间的浪费。但是,如果只是在原生函数内部使用的对象,那么比起使用麻烦的crowbar对象,使用C的 malloc()更为方便。因此,实际情况下像上面那样的问题并不多见,所以还是维持了这样的设计。

4.4.5 原生指针类型的修改

crowbar book_ver.0.1的原生指针类型的值(CRB_Value结构体)中包含了 void*的指针和数据类型的标识信息。在3.3.9节中提到过,这个方法在“一个地方用fclose()关闭了文件指针,可能还会在另一个地方被使用”的情况下会存在问题。

在book_ver.0.2中,原生指针类型的值不再指向实际的原生对象(FILE结构体等),而是在中间加入了一个crowbar对象进行“隔离”(如图4-12)。

figure_0163_0043

图4-12 原生指针的构造(修改版)

运用上面的方法,例如在调用 fclose()的时候,将 CRB_Object中指向原生对象的指针同时置为 NULL,就能够立刻判断出文件已被关闭。也许有人会想,在以前的做法中,没有将 CRB_Value直接指向 FILE结构体,而是在它们中间夹了一个别的结构体,不是也起到同样的作用了吗?假设真的这么做了,那么在对这个结构体进行 free()的时机就又成了一个问题。何不借着实现GC的机会,让它们中间夹一个可以作为GC目标的对象呢?

顺便说一下,虽然这样一来就可以实现针对原生指针类型的 终结器  (finalizer)了(GC在释放原生指针类型的对象时调用之前注册的函数就可以了),但是由于mark-sweep型的GC中终结器“不知何时启动”,因此使用crowbar的用户最好不要编写依赖于终结器的程序。但是对于一门编程语言来说,提供终结器也不是什么坏事。

注 释

[1]. 不考虑crowbar的MEM实际会占用更大的管理空间。

[2]. C语言中只有“数组的数组”,数组也不属于引用类型,在这里道理是一样的。

[3]. 现在的crowbar并没有在编译时处理多个字符串常量的加法运算(虽然可以这么做),所以在这个例子中,使用字符串常量进行加法运算的效果与使用字符串变量相同,这里并不是为了说明变量和常量的差异。

[4]. 也不知道Java这样的设计方便性在哪里?可能是会提升 intern()的效率吧。

[5]. 这种内存溢出会随着运行时间慢慢泄漏。这种性质的内存泄漏,根据程序的用途也许会起到致命的作用,我认为在这种类型的conserva-tive GC中,总会有一定几率造成对象泄漏,并且在数量上也会占到栈的一定比例。

[6]. 即在函数被调用时。——译者注

[7]. 关于动态作用域的策略,“对一个名字x的使用指向的是最近被调用但还没有终止且声明了x的过程中的这个声明”。(摘自“龙书”[1]P19)——译者注

[8]. C语言使用malloc()申请内存空间,这里指可以以任意顺序来申请/释放的内存区域。

[9]. 去除这些没用的间隙,把对象存储空间变紧凑的操作叫作精简(compaction)。