由Scheme到C下一步就是如何为Guile定义C函数,使此C函数能转换为Scheme过程。这样我们就不得不提到一个Guile的C API中经常用到的自定义结构体类型--SCM,几乎Guile中所有的C API都要用到这一结构类型,它是Guile中的数据类型的通用体,也就是说,Guile在解释Scheme语言代码时,将所有的Scheme语言中的数据类型变量都做为一个SCM结构体类型来处理。Guile提供了一系列函数来将SCM结构转换为C语言其它类型,如整型、浮点型、字符指针类型等;同样,我们也可以将C语言中的整型、浮点型、字符指针类型变量转换为SCM结构,这样Guile中的C API就可以正常处理这一变量了。
如果我们正常定义一个C函数int add(int x, int y) ,转换为Scheme语言过程则应当是(add x y),而我们定义的C函数并不能为Guile正确执行,Guile所执行的应当是SCM add (SCM x, SCM y) 这样的函数,因为在Guile在执行(add x y)过程时,将x和y做为一个SCM结构来处理,返回值也应当是一个SCM,所以我们应当设计SCM add(SCM x, SCM y)这样的C函数。
如此,则我们将函数 void gdk_draw_point(GdkWindow *window, GdkColor gc, int x, int y) ,应当简单的封装为 SCM draw_point(SCM x, SCM y) 以便于Guile解释执行,而其中的GdkWindow *area和GdkColor gc_front则应首先定义为全局变量,直接在函数中调用,函数代码应当是:
1
2
3
4
5
6
7
| static SCM draw_point(SCM x, SCM y)
{
gdk_draw_point(area, gc_front,
SCM_INUM(x), SCM_INUM(y));
gdk_draw_point(map, gc_front,
SCM_INUM(x), SCM_INUM(y));
}
|
其中的area和gc_front分别代表绘图区和前景颜色,map则代表用在保存时中用到的pixmap,在构建绘图控件时定义。由于其返回类型是空值,所以可以不做返回操作。还有就是这些将转换并封装为Scheme语言过程的函数都应当定义为静态的。
其中的SCM_INUM宏是Guile库内部定义的,可以将SCM结构转换为一个整型数;这样的宏还有SCM_STRING_CHARS和SCM_SYMBOL_CHARS,其中的SCM_STRING_CHARS宏将Scheme语言中的字符串型SCM结构转换为字符型指针;而SCM_SYMBOL_CHARS宏则将符号型SCM结构转换为字符型指针,我们在绘制输出字符串时就用到SCM_STRING_CHARS宏,这是因为我们设计的输出字符串的Scheme过程为(_string x y "some string here")。输出字符串的函数代码定义如下:
1
2
3
4
5
6
7
8
9
10
| //定义绘制输出字符串函数
static SCM draw_string(SCM x, SCM y, SCM str)
{
PangoLayout *layout =
gtk_widget_create_pango_layout(area, SCM_STRING_CHARS(str));
gdk_draw_layout(area->window, gc_front,
SCM_INUM(x), SCM_INUM(y), layout);
gdk_draw_layout(map, gc_front,
SCM_INUM(x), SCM_INUM(y), layout);
}
|
这其中最复杂的就是绘制多边形函数,我设计导出的Scheme过程为(_polygon point-list, point-number filled),第一个参数为点列表,第二个参数是点数,第三个参数为是否添充。用SCM_NFALSEP宏可以将Scheme语言中的#f转换为0,#t转换为1,这样是否添充这个问题就解决了;而gdk_draw_polygon函数需要的GdkPoint指针数组需要分配内存来实现,我们设计的参数是一个列表,可以用scm_list_ref来取值,关键是这个函数的第二个参数并不是一个简单的整数,而是一个SCM结构,可以用scm_int2num函数将一个C整数转换为SCM结构,再用SCM_CAR和SCM_CDR分别取点对的左侧和右侧分别赋给GdkPoint结构中的x和y,绘图后释放内存,达到目的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| //绘制多边形函数,参数为点数组,点数,是否添充
static SCM draw_polygon(SCM spoints, SCM number, SCM filled)
{
int i ;
GdkPoint *points = g_new(GdkPoint, SCM_INUM(number)) ; //为点分配内存
for(i=0; i<SCM_INUM(number); i++)
{
points.x = SCM_INUM(SCM_CAR(scm_list_ref(spoints, scm_int2num(i))));
points.y = SCM_INUM(SCM_CDR(scm_list_ref(spoints, scm_int2num(i))));
//循环赋值,关键是将C中的int转换为SCM的 scm_int2num 函数
}
gdk_draw_polygon(area->window, gc_front, SCM_NFALSEP(filled), points, SCM_INUM(number));
g_free(points); //释放内存
}
|
更多函数定义请看此应用的C源码,可以到参考资料中下载。
将C函数导出为Scheme过程我们可以将上面定义好的C函数导出Scheme过程,这一功能由Guile中的C函数scm_c_define_gsubr实现,这个函数需要5个参数,第一个参数是要转换为成的Scheme过程名,第2,3,4三个参数与要转换的Scheme过程的参数数量有关,第2个参数表示必需的参数的数量,第3个参数表示可选的参数的数量,第4个参数表示参数的数量是否可变,如果此参数置0则参数的总量为必需参数和可选参数的和,如果此参数非0,则参数的总量为必需参数与可选参数的和再加1;最后一个参数是要转换的C函数名指针,这一函数名必须按上面的要求,即返回类型和参数均为SCM结构类型。
我们的应用最后导出六个原始的绘图过程,分别是:
1、_point,画点,两个参数 x, y ;
2、_line,画线,四个参数 x1, y1, x2, y2 ;
3、_rectangle,画矩形,五个参数 x, y, width, height, filled ;
4、_arc,画弧线,七个参数 x, y, width, height, angle1, angle2, filled ;
5、_polygon,画多边形,三个参数 point-list, point-number, filled ;
6、_string,绘制字符串文本,三个参数 x, y, string;
7、_color,设定绘图颜色,三个参数 red, green, blue 。
导出代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| //编码绘图初始化过程
void draw_init(void)
{
scm_init_guile();
//初始化guile
scm_c_define_gsubr("_rectangle", 5, 0, 0, draw_rect);
scm_c_define_gsubr("_point", 2, 0, 0, draw_point);
scm_c_define_gsubr("_line", 4, 0, 0, draw_line);
scm_c_define_gsubr("_arc", 7, 0, 0, draw_arc);
scm_c_define_gsubr("_polygon", 3, 0, 0, draw_polygon);
scm_c_define_gsubr("_string", 3, 0, 0, draw_string);
scm_c_define_gsubr("_color", 3, 0, 0, set_front_color);
//定义六个原始绘图过程
}
|
当我们将上面的C函数转换为Scheme语言过程后,我们就可以在我们的Scheme语言代码中加入这些过程了,但要达到绘图效果还得有必须的一步,就是让我们的应用执行这些Scheme语言代码,以达到绘图效果。这样我们就必需在运行按钮点击信号的回调函数中加入执行Scheme语言代码的功能,这就用到了Guile的C API中的scm_c_primitive_load函数,它的功能就是打开并运行一个Scheme语言代码文件,也就是我们的绘图文件。如此我们就真正达到了用代码画图形的功能了。(关于图像的保存读者可自行阅读代码) |