|
2005 年 11 月 21 日
目前應(yīng)用最廣泛的技術(shù)之一是編寫(xiě)生成其他程序或部分程序的程序。因此十分有必要學(xué)習(xí)為什么要采用元編程,以及元編程都有哪些組件(文本宏語(yǔ)言,專用代碼生成器)。在本文中,您將學(xué)習(xí)到如何構(gòu)建一個(gè)代碼生成器,并詳細(xì)了解如何使用 Scheme 編寫(xiě)對(duì)語(yǔ)言敏感的宏。
用來(lái)生成代碼的程序有時(shí)被稱為 元程序(metaprogram);編寫(xiě)這種程序就稱為 元編程(metaprogramming)。編寫(xiě)這種輸出代碼的程序可以有無(wú)數(shù)的應(yīng)用。
本文將介紹為什么會(huì)考慮進(jìn)行元編程,并介紹這種技術(shù)的一些組件 —— 我們將深入介紹文本宏語(yǔ)言(textual macro language),了解專用的代碼生成器,并討論如何構(gòu)建這些工具,最后研究如何使用 Scheme 編寫(xiě)對(duì)語(yǔ)言敏感的宏。
元編程的不同用法
首先,可以編寫(xiě)一些程序來(lái)提前生成一些數(shù)據(jù)供運(yùn)行時(shí)使用。例如,如果您正在開(kāi)發(fā)一個(gè)游戲,并且希望使用一個(gè)所有 8 位整數(shù)的正弦值的查詢表,既可以每次都執(zhí)行正弦計(jì)算的操作,也可以讓程序在啟動(dòng)時(shí)構(gòu)建這樣的一張表在運(yùn)行時(shí)使用,或者編寫(xiě)一個(gè)程序在編譯之前為這個(gè)表生成定制代碼。盡管對(duì)于少量的數(shù)據(jù)來(lái)說(shuō)在運(yùn)行時(shí)構(gòu)建這張表是可能的,但是有些任務(wù)則可能會(huì)使得程序啟動(dòng)非常緩慢。在這種情況中,編寫(xiě)一個(gè)程序來(lái)構(gòu)建一張靜態(tài)表通常是最好的解決方案。
其次,如果您有一個(gè)很大的應(yīng)用程序,這個(gè)程序有很多函數(shù)都包括了很多樣板文件,那么就可以創(chuàng)建一個(gè)小型的語(yǔ)言,它可以生成這些樣板代碼,讓您可以只實(shí)現(xiàn)重要的部分?,F(xiàn)在,如果可以,最好是能夠?qū)⑦@些樣板部分抽象成一個(gè)函數(shù)。但是通常來(lái)說(shuō),這些樣板代碼并不會(huì)如此精美??赡苊總€(gè)實(shí)例中都需要聲明一些變量,可能需要注冊(cè)錯(cuò)誤處理程序,或者有一些樣板文件必須在某些情況中插入一些代碼。所有這些都使得簡(jiǎn)單的函數(shù)調(diào)用是不可能的。在這種情況中,通常創(chuàng)建一個(gè)小型的語(yǔ)言來(lái)更簡(jiǎn)單地利用樣板文件的代碼。這種小型的語(yǔ)言可以在編譯之前被轉(zhuǎn)換成普通的源代碼語(yǔ)言。
最后,有很多編程語(yǔ)言都可以編寫(xiě)非常復(fù)雜的語(yǔ)句來(lái)真正實(shí)現(xiàn)一些功能。代碼生成程序可以對(duì)這種語(yǔ)句進(jìn)行簡(jiǎn)化,并節(jié)省很多輸入的工作,這可以防止大量的輸入錯(cuò)誤,因?yàn)闇p少了很多輸入錯(cuò)誤內(nèi)容的機(jī)會(huì)。
作為語(yǔ)言可以有很多特性,代碼生成程序就不需要這么多了。一種語(yǔ)言中的標(biāo)準(zhǔn)特性在另外一種語(yǔ)言中可能只能通過(guò)代碼生成程序?qū)崿F(xiàn)。然而,語(yǔ)言設(shè)計(jì)不充分并不是需要代碼生成程序的唯一原因。維護(hù)簡(jiǎn)單也是一個(gè)原因。
文本宏語(yǔ)言基礎(chǔ)
代碼生成程序允許您開(kāi)發(fā)并使用小型的、領(lǐng)域特有的語(yǔ)言,這樣比直接在目標(biāo)語(yǔ)言中開(kāi)發(fā)這種功能更容易編寫(xiě)和維護(hù)。
用來(lái)創(chuàng)建領(lǐng)域特有語(yǔ)言的工具通常稱為 宏語(yǔ)言(macro language)。本文介紹了幾種宏語(yǔ)言的方法,并介紹了如何改進(jìn)代碼。
C 預(yù)處理器(CPP)
首先讓我們來(lái)看一下涉及文本宏編程的元編程。文本宏(textual macro) 是可以直接影響編程語(yǔ)言中的文本的宏,它們并不需要了解或處理語(yǔ)言的意義。兩個(gè)最廣泛使用的文本宏系統(tǒng)是 C 預(yù)處理器和 M4 宏處理器。
如果您曾經(jīng)使用 C 進(jìn)行過(guò)編程,那么可能處理過(guò) C 語(yǔ)言中的 #define 宏。文本宏的擴(kuò)展雖然不甚理想,但在很多沒(méi)有更好的代碼生成能力的語(yǔ)言中,這是用來(lái)進(jìn)行基本元編程的一種簡(jiǎn)單方法。清單 1 給出了一個(gè) #define 宏的例子: 清單 1. 用來(lái)交換兩個(gè)值的宏
#define SWAP(a, b, type) { type __tmp_c; c = b; b = a; a = c; }
|
這個(gè)宏可以交換兩個(gè)給定類型的值。由于幾個(gè)原因,這最好是作為一個(gè)宏來(lái)實(shí)現(xiàn):
- 對(duì)于這種簡(jiǎn)單的操作來(lái)說(shuō),函數(shù)調(diào)用的開(kāi)銷太大。
- 需要向函數(shù)傳遞變量的地址而不是變量的值。(這并不是很大的問(wèn)題,但是傳遞值會(huì)使函數(shù)調(diào)用比較混亂,并且編輯器就無(wú)法將這些值保存在寄存器中了。)
- 對(duì)于每種需要交換的類型來(lái)說(shuō),都需要定義一個(gè)不同的函數(shù)。
清單 2 給出了一個(gè)使用宏的例子: 清單 2. SWAP 宏的使用
#define SWAP(a, b, type) { type __tmp_c; c = b; b = a; a = c; }
int main()
{
int a = 3;
int b = 5;
printf("a is %d and b is %d\n", a, b);
SWAP(a, b, int);
printf("a is now %d and b is now %d\n", a, b);
return 0;
}
|
當(dāng)運(yùn)行 C 預(yù)處理器時(shí),它會(huì)將 SWAP(a, b, int) 替換成 { int __tmp_c; __tmp_c = b; b = a; a = __tmp_c; }。
文本替換是一種有效但是卻非常有限的特性。這種特性有以下問(wèn)題:
- 文本替換在與其他表達(dá)式一起使用時(shí),可能會(huì)變得非?;靵y。
- C 預(yù)處理器對(duì)于自己的宏只允許使用有限數(shù)目的參數(shù)。
- 由于 C 語(yǔ)言的類型系統(tǒng),通常需要對(duì)不同類型的參數(shù)定義不同的宏,至少必須傳遞一個(gè)參數(shù)類型作為參數(shù)。
- 由于只進(jìn)行文本替換,因此如果這與傳遞給它的參數(shù)沖突,C 語(yǔ)言就無(wú)法智能地對(duì)臨時(shí)變量重新進(jìn)行命名。如果傳遞
__tmp_c 變量,那么我們這個(gè)宏就會(huì)完全失敗了。
在表達(dá)式中合并宏的問(wèn)題使得編寫(xiě)宏非常困難。例如,假設(shè)已經(jīng)定義了下面這個(gè) MIN 宏,它返回兩個(gè)值中的較小值: 清單 3. 返回兩個(gè)值中較小值的宏
#define MIN(x, y) ((x) > (y) ? (y) : (x))
|
首先,您可能會(huì)奇怪為什么此處使用了這么多的括號(hào)。原因是操作符的優(yōu)先順序。例如我們要執(zhí)行 MIN(27, b=32),如果沒(méi)有這些括號(hào),這個(gè)表達(dá)式就會(huì)擴(kuò)展成 27 > b = 32 ? b = 32 : 27,這會(huì)產(chǎn)生一個(gè)編譯器錯(cuò)誤,因?yàn)榘凑詹僮鞣膬?yōu)先順序,27 > b 會(huì)連接在一起。如果在定義宏時(shí)使用了這些括號(hào),那它就可以正常工作了。
不幸的是,這里還有一個(gè)問(wèn)題。任何作為參數(shù)調(diào)用的函數(shù)每次都會(huì)被列到右邊。記住,預(yù)處理器并不了解 C 語(yǔ)言的任何內(nèi)容,它只是簡(jiǎn)單地進(jìn)行文本替換。因此,如果執(zhí)行一條語(yǔ)句 MIN(do_long_calc(), do_long_calc2()),它就會(huì)擴(kuò)展成 ( (do_long_calc()) > (do_long_calc2()) ? (do_long_calc2()) : (do_long_calc()))。這樣執(zhí)行的時(shí)間會(huì)更長(zhǎng),因?yàn)槊總€(gè)計(jì)算都至少要執(zhí)行兩次。
如果這些計(jì)算有某些副作用(例如打印、修改全局變量等),那情況就更加嚴(yán)重了,因?yàn)檫@些副作用都會(huì)被處理兩次。如果這些函數(shù)每次調(diào)用時(shí)所返回的結(jié)果都不相同,那么這種“多次調(diào)用”的問(wèn)題甚至?xí)屵@個(gè)宏返回錯(cuò)誤的結(jié)果。
更多有關(guān) C 預(yù)處理器宏編程的內(nèi)容可以在 CPP 手冊(cè)中看到(請(qǐng)參閱 參考資料 一節(jié)中的鏈接)。
M4 宏預(yù)處理器
M4 宏處理器是最高級(jí)的文本宏處理系統(tǒng)之一。它的聲望主要是由于這是流行的 sendmail 配置文件所使用的輔助工具。
sendmail 的配置既不有趣,也不簡(jiǎn)單。sendmail 的配置文件就有一整本書(shū)專門(mén)來(lái)講解。然而,sendmail 的創(chuàng)造者編寫(xiě)了一些 M4 宏來(lái)簡(jiǎn)化這個(gè)處理過(guò)程。在這些宏中,您可以簡(jiǎn)單地指定某些特定的參數(shù),M4 處理器可以對(duì)一個(gè)樣板文件進(jìn)行操作,這個(gè)文件是特定于本地安裝和 sendmail 的通用設(shè)置的。這樣就可以為您提供一個(gè)配置文件了。
例如,清單 4 給出了一個(gè)典型的 sendmail 配置文件的 M4 宏: 清單 4. 使用 M4 宏的樣例 sendmail 配置文件
divert(-1)
include(`/usr/share/sendmail-cf/m4/cf.m4‘)
VERSIONID(`linux setup for my Linux dist‘)dnl
OSTYPE(`linux‘)
define(`confDEF_USER_ID‘,``8:12‘‘)dnl
undefine(`UUCP_RELAY‘)dnl
undefine(`BITNET_RELAY‘)dnl
define(`PROCMAIL_MAILER_PATH‘,`/usr/bin/procmail‘)dnl
define(`ALIAS_FILE‘, `/etc/aliases‘)dnl
define(`UUCP_MAILER_MAX‘, `2000000‘)dnl
define(`confUSERDB_SPEC‘, `/etc/mail/userdb.db‘)dnl
define(`confPRIVACY_FLAGS‘, `authwarnings,novrfy,noexpn,restrictqrun‘)dnl
define(`confAUTH_OPTIONS‘, `A‘)dnl
define(`confTO_IDENT‘, `0‘)dnl
FEATURE(`no_default_msa‘,`dnl‘)dnl
FEATURE(`smrsh‘,`/usr/sbin/smrsh‘)dnl
FEATURE(`mailertable‘,`hash -o /etc/mail/mailertable.db‘)dnl
FEATURE(`virtusertable‘,`hash -o /etc/mail/virtusertable.db‘)dnl
FEATURE(redirect)dnl
FEATURE(always_add_domain)dnl
FEATURE(use_cw_file)dnl
FEATURE(use_ct_file)dnl
FEATURE(local_procmail,`‘,`procmail -t -Y -a $h -d $u‘)dnl
FEATURE(`access_db‘,`hash -T<TMPF> -o /etc/mail/access.db‘)dnl
FEATURE(`blacklist_recipients‘)dnl
EXPOSED_USER(`root‘)dnl
DAEMON_OPTIONS(`Port=smtp,Addr=127.0.0.1, Name=MTA‘)
FEATURE(`accept_unresolvable_domains‘)dnl
MAILER(smtp)dnl
MAILER(procmail)dnl
Cwlocalhost.localdomain
|
您并不需要理解這些配置的具體含義,只需要知道存在這個(gè)文件就可以了。在 M4 宏處理這個(gè)文件之后,就會(huì)生成大約 1,000 行的配置。
類似地,autoconf 使用 M4 宏基于簡(jiǎn)單的宏來(lái)生成 shell 腳本。如果您曾經(jīng)在安裝程序時(shí)首先輸入 ./configure,那么就可能使用了一個(gè)由 autoconf 宏所生成的程序。清單 5 是一個(gè)簡(jiǎn)單的 autoconf 程序,它生成了一個(gè)超過(guò) 3,000 行的 configure 程序: 清單 5. 使用 M4 宏的 autoconf 腳本
AC_INIT(hello.c)
AM_CONFIG_HEADER(config.h)
AM_INIT_AUTOMAKE(hello,0.1)
AC_PROG_CC
AC_PROG_INSTALL
AC_OUTPUT(Makefile)
|
在宏處理器運(yùn)行這個(gè)腳本時(shí),會(huì)創(chuàng)建一個(gè) shell 腳本,它會(huì)進(jìn)行標(biāo)準(zhǔn)的配置檢查,查找標(biāo)準(zhǔn)的路徑和編譯器命令,并從模板中為您構(gòu)建 config.h 和 Makefile 文件。
這些 M4 宏處理器的詳細(xì)信息太過(guò)復(fù)雜,我們就不再在本文中進(jìn)行介紹了,不過(guò)在 參考資料 一節(jié)中給出了有關(guān) M4 宏處理器及其在 sendmail 和 autoconf 中的用法的鏈接。
用來(lái)編寫(xiě)程序的程序
現(xiàn)在讓我們把注意力從通用的文本替換程序轉(zhuǎn)移到專用的代碼生成器上來(lái)。我們將介紹幾個(gè)例子,了解一下樣例用法,并構(gòu)建一個(gè)代碼生成器。
代碼生成器的考慮因素
GNU/Linux 系統(tǒng)提供了幾個(gè)用來(lái)編寫(xiě)程序的程序。最常見(jiàn)的有:
- Flex,這是一個(gè)詞匯分析器生成器
- Bison,語(yǔ)法分析器生成器
- Gperf,一個(gè)很好的 hash 函數(shù)生成器
這些工具都可以為 C 語(yǔ)言生成一些文件。您可能會(huì)納悶為什么這些都是作為代碼生成器實(shí)現(xiàn)的,而不是作為函數(shù)實(shí)現(xiàn)的。原因有幾個(gè)方面:
- 這些函數(shù)的輸入都非常復(fù)雜,不容易使用一種有效的 C 代碼格式來(lái)表示。
- 這些程序會(huì)為操作生成很多靜態(tài)的查找表,因此在預(yù)編譯時(shí)一次生成這些表比每次調(diào)用這個(gè)程序時(shí)都生成這些表更好。
- 這些系統(tǒng)的很多功能都是可以使用某些特定位置的任意代碼進(jìn)行定制的。這些代碼然后就可以使用代碼生成器所生成的結(jié)構(gòu)中的變量和功能了,而不需要手工生成這些變量。
每個(gè)工具都著重于構(gòu)建一種特定類型的系統(tǒng)。Bison 用來(lái)生成語(yǔ)法分析器;Flex 用來(lái)生成詞匯分析器。其他工具用來(lái)實(shí)現(xiàn)編程中的自動(dòng)化部分。
例如,將數(shù)據(jù)庫(kù)訪問(wèn)方法集成到一種語(yǔ)言中通常非常繁瑣。要讓這個(gè)過(guò)程變得又簡(jiǎn)單、又標(biāo)準(zhǔn)化,那么嵌入式 SQL 就是一個(gè)很好的元編程系統(tǒng),可以在 C 語(yǔ)言中簡(jiǎn)單地合并數(shù)據(jù)庫(kù)訪問(wèn)的功能。
雖然在 C 語(yǔ)言中有很多庫(kù)可以用來(lái)訪問(wèn)數(shù)據(jù)庫(kù),但是使用諸如嵌入式 SQL 之類的代碼生成器可以使合并 C 和數(shù)據(jù)庫(kù)訪問(wèn)的功能更加簡(jiǎn)單:它將 SQL 實(shí)體的功能作為語(yǔ)言的一種擴(kuò)展合并到了 C 語(yǔ)言中。然而,很多嵌入式 SQL 的實(shí)現(xiàn)通常都是一些專用的宏處理器,可以生成 C 程序作為輸出結(jié)果。使用嵌入式 SQL 可以讓對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)比直接使用庫(kù)函數(shù)來(lái)訪問(wèn)數(shù)據(jù)庫(kù)更加自然、直觀,而且程序員可以更少犯錯(cuò)誤。使用嵌入式 SQL,數(shù)據(jù)庫(kù)編程的復(fù)雜性可以通過(guò)一些宏子語(yǔ)言來(lái)屏蔽。
如何使用代碼生成器
為了了解代碼生成器是如何工作的,讓我們先來(lái)看一個(gè)簡(jiǎn)短的嵌入式 SQL 程序。為了實(shí)現(xiàn)這種功能,需要使用一個(gè)嵌入式 SQL 的處理程序。PostgreSQL 就提供了一個(gè)嵌入式 SQL 的編譯器 ecpg。要運(yùn)行這個(gè)程序,需要在 PostgreSQL 中創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)“test”。然后在這個(gè)數(shù)據(jù)庫(kù)中執(zhí)行下面的命令: 清單 6. 樣例程序的數(shù)據(jù)庫(kù)創(chuàng)建腳本
create table people (id serial primary key, name varchar(50));
insert into people (name) values (‘Tony‘);
insert into people (name) values (‘Bob‘);
insert into people (name) values (‘Mary‘);
|
清單 7 是一個(gè)簡(jiǎn)單的程序,它從數(shù)據(jù)庫(kù)中讀出數(shù)據(jù)的內(nèi)容,并將其打印到屏幕上,在打印時(shí)對(duì) name 域進(jìn)行排序: 清單 7. 嵌入式 SQL 程序的例子
#include <stdio.h>
int main()
{
/* Setup database connection -- replace postgres/password w/ the
username/password on your system*/
EXEC SQL CONNECT TO unix:postgresql://localhost/test USER postgres/password;
/* These variables are going to be used for temporary storage w/ the database */
EXEC SQL BEGIN DECLARE SECTION;
int my_id;
VARCHAR my_name[200];
EXEC SQL END DECLARE SECTION;
/* This is the statement we are going to execute */
EXEC SQL DECLARE test_cursor CURSOR FOR
SELECT id, name FROM people ORDER BY name;
/* Run the statement */
EXEC SQL OPEN test_cursor;
EXEC SQL WHENEVER NOT FOUND GOTO close_test_cursor;
while(1) /* our previous statement will handle exitting the loop */
{
/* Fetch the next value */
EXEC SQL FETCH test_cursor INTO :my_id, :my_name;
printf("Fetched ID is %d and fetched name is %s\n", my_id, my_name.arr);
}
/* Cleanup */
close_test_cursor:
EXEC SQL CLOSE test_cursor;
EXEC SQL DISCONNECT;
return 0;
}
|
如果您以前曾經(jīng)在 C 語(yǔ)言中使用普通的數(shù)據(jù)庫(kù)庫(kù)函數(shù)編寫(xiě)過(guò)訪問(wèn)數(shù)據(jù)庫(kù)的程序,就會(huì)看出這是一種非常自然的編寫(xiě)代碼的方法。正常的 C 編碼不允許返回多個(gè)任意類型的值,但是 EXEC SQL FETCH 卻可以返回多行結(jié)果。
要編譯并運(yùn)行這個(gè)程序,只需要將其保存到 test.pgc 文件中,并運(yùn)行下面的命令: 清單 8. 編譯嵌入式 SQL 程序
ecpg test.pgc
gcc test.c -lecpg -o test
./test
|
構(gòu)建代碼生成器
現(xiàn)在您已經(jīng)見(jiàn)過(guò)了幾種代碼生成器,了解了這些代碼生成器可以實(shí)現(xiàn)怎樣的功能,接下來(lái)我們應(yīng)該開(kāi)始編寫(xiě)一個(gè)小型的代碼生成器了??梢跃帉?xiě)的最簡(jiǎn)單的可用代碼生成器也許就是構(gòu)建一個(gè)靜態(tài)查找表。通常,為了在 C 編程中構(gòu)建快速的函數(shù),只需要簡(jiǎn)單地創(chuàng)建一個(gè)快速查找表,其中保存了所有的結(jié)果。這意味著可能需要手工提前計(jì)算好(這會(huì)浪費(fèi)很多時(shí)間),也可以在運(yùn)行時(shí)構(gòu)建(這會(huì)浪費(fèi)用戶的時(shí)間)。
在這個(gè)例子中,我們將構(gòu)建一個(gè)代碼生成器,它要對(duì)一個(gè)整數(shù)執(zhí)行一個(gè)或一組函數(shù),并為結(jié)果構(gòu)建一個(gè)查找表。
要思考如何構(gòu)建這樣一個(gè)程序,讓我們從最后入手,并從后往前逐一解決問(wèn)題。假設(shè)我們希望得到這樣一個(gè)查找表:它返回 5 到 20 之間各個(gè)數(shù)字的平方根。我們可以編寫(xiě)一個(gè)簡(jiǎn)單的程序來(lái)生成這樣一個(gè)查找表,例如: 清單 9. 生成并使用一個(gè)平方根查找表
/* our lookup table */
double square_roots[21];
/* function to load the table at runtime */
void init_square_roots()
{
int i;
for(i = 5; i < 21; i++)
{
square_roots[i] = sqrt((double)i);
}
}
/* program that uses the table */
int main ()
{
init_square_roots();
printf("The square root of 5 is %f\n", square_roots[5]);
return 0;
}
|
現(xiàn)在,要將這些結(jié)果轉(zhuǎn)換成一個(gè)靜態(tài)初始化的數(shù)組,我們需要?jiǎng)h除這個(gè)程序的前半部分,并將其替換成手工計(jì)算出來(lái)的結(jié)果,如下所示: 清單 10. 帶靜態(tài)查找表的平方根程序
double square_roots[] = {
/* these are the ones we skipped */ 0.0, 0.0, 0.0, 0.0, 0.0
2.236068, /* Square root of 5 */
2.449490, /* Square root of 6 */
2.645751, /* Square root of 7 */
2.828427, /* Square root of 8 */
3.0, /* Square root of 9 */
...
4.472136 /* Square root of 20 */
};
|
我們需要的是這樣一個(gè)程序,它可以生成這些結(jié)果,并將其輸出到上面這樣的表中,這樣就可以在編譯時(shí)加載了。
下面讓我們分析一下要解決哪些問(wèn)題:
- 數(shù)組名
- 數(shù)組類型
- 起始索引
- 結(jié)束索引
- 忽略項(xiàng)的缺省值
- 計(jì)算最終值的表達(dá)式
這些都非常簡(jiǎn)單,并且進(jìn)行了很好的定義 —— 它們可以作為一個(gè)簡(jiǎn)單的列表進(jìn)行輸出。因此我們可能會(huì)希望執(zhí)行宏調(diào)用,將這些元素合并到一個(gè)使用冒號(hào)進(jìn)行分隔的列表中,如下所示: 清單 11. 生成編譯時(shí)平方根表的理想方法
/* sqrt.in */
/* Our macro invocation to build us the table. The format is: */
/* TABLE:array name:type:start index:end index:default:expression */
/* VAL is used as the placeholder for the current index in the expression */
TABLE:square_roots:double:5:20:0.0:sqrt(VAL)
int main()
{
printf("The square root of 5 is %f\n", square_roots[5]);
return 0;
}
|
現(xiàn)在我們只需要一個(gè)程序?qū)⑦@個(gè)宏轉(zhuǎn)換成標(biāo)準(zhǔn)的 C 語(yǔ)言就可以了。對(duì)于這個(gè)簡(jiǎn)單的例子來(lái)說(shuō),我們將使用 Perl 來(lái)實(shí)現(xiàn)這個(gè)程序,因?yàn)樗梢詫?duì)字符串中的用戶代碼進(jìn)行評(píng)測(cè),其語(yǔ)法也與 C 語(yǔ)言非常類似。這樣我們就可以動(dòng)態(tài)加載并處理用戶代碼了。
代碼生成器應(yīng)該處理宏的聲明,但是對(duì)于所有非宏的部分都應(yīng)該不加任何修改地傳遞。因此,宏處理器的基本組織應(yīng)該是:
- 讀入一行。
- 判斷該行是否應(yīng)該進(jìn)行處理?
- 如果應(yīng)該,就對(duì)該行進(jìn)行處理,并生成輸出結(jié)果。
- 否則,就簡(jiǎn)單地將這一行的內(nèi)容不加任何修改,直接拷貝到輸出中。
清單 12 是創(chuàng)建這個(gè)表生成器所使用的 Perl 代碼: 清單 12. 這個(gè)表宏的代碼生成器
#!/usr/bin/perl
#
#tablegen.pl
#
##Puts each program line into $line
while(my $line = <>)
{
#Is this a macro invocation?
if($line =~ m/TABLE:/)
{
#If so, split it apart into its component pieces
my ($dummy, $table_name, $type, $start_idx, $end_idx, $default,
$procedure) = split(m/:/, $line, 7);
#The main difference between C and Perl for mathematical expressions is that
#Perl prefixes its variables with a dollar sign, so we will add that here
$procedure =~ s/VAL/\$VAL/g;
#Print out the array declaration
print "${type} ${table_name} [] = {\n";
#Go through each array element
foreach my $VAL (0 .. $end_idx)
{
#Only process an answer if we have reached our starting index
if($VAL >= $start_idx)
{
#evaluate the procedure specified (this sets $@ if there are any errors)
$result = eval $procedure;
die("Error processing: $@") if $@;
}
else
{
#if we haven‘t reached the starting index, just use the default
$result = $default;
}
#Print out the value
print "\t${result}";
#If there are more to be processed, add a comma after the value
if($VAL != $end_idx)
{
print ",";
}
print "\n"
}
#Finish the declaration
print "};\n";
}
else
{
#If this is not a macro invocation, just copy the line directly to the output
print $line;
}
}
|
要運(yùn)行這個(gè)程序,請(qǐng)執(zhí)行下面的命令: 清單 13. 運(yùn)行代碼生成器
./tablegen.pl < sqrt.in > sqrt.c
gcc sqrt.c -o sqrt
./a.out
|
這樣只需要?jiǎng)偛艅?chuàng)建的這個(gè)簡(jiǎn)單代碼生成器中的幾行代碼,我們就可以極大地簡(jiǎn)化編程任務(wù)。使用這一個(gè)宏,就可以節(jié)省很多編程的工作,它可以生成一個(gè)使用整數(shù)進(jìn)行索引的數(shù)學(xué)表。我們還要實(shí)現(xiàn)另外一些任務(wù):讓這個(gè)表包含完整的結(jié)構(gòu)定義;還要確保這個(gè)數(shù)組前面沒(méi)有空項(xiàng),這樣就不會(huì)浪費(fèi)空間。
使用 Scheme 編寫(xiě)對(duì)語(yǔ)言敏感的宏
盡管代碼生成器可以理解一點(diǎn)兒目標(biāo)語(yǔ)言的知識(shí),但是它們通常都不是完整的語(yǔ)法分析器,不重新編寫(xiě)一個(gè)完整的編譯器是無(wú)法全面考慮目標(biāo)語(yǔ)言的。
然而,如果有一種語(yǔ)言已經(jīng)使用一個(gè)簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)進(jìn)行了表示,那么這種情況就可以簡(jiǎn)化了。在 Scheme 編程語(yǔ)言中,這種語(yǔ)言本身可以表示成一個(gè)鏈表,并且 Scheme 編程語(yǔ)言就是為進(jìn)行列表處理而開(kāi)發(fā)的。這使得 Scheme 非常適合于創(chuàng)建被轉(zhuǎn)換的程序,要對(duì)程序進(jìn)行分析并不需要大量的處理,Scheme 本身就是一種列表處理語(yǔ)言。
實(shí)際上,Scheme 用來(lái)實(shí)現(xiàn)轉(zhuǎn)換的功能已經(jīng)超出了本文的范圍。Scheme 標(biāo)準(zhǔn)定義了一種專門(mén)用來(lái)簡(jiǎn)化對(duì)其他語(yǔ)言進(jìn)行擴(kuò)展的宏語(yǔ)言。大部分 Scheme 的實(shí)現(xiàn)都提供了一些特性來(lái)輔助構(gòu)建代碼生成程序。
讓我們重新研究一下 C 宏中的問(wèn)題。使用 SWAP 宏,首先必須要顯式地說(shuō)明要交換的值的類型,必須要為臨時(shí)變量使用一個(gè)名字,并且要確保這個(gè)名字沒(méi)有在其他地方使用。讓我們來(lái)看一下 Scheme 的等效代碼,以及 Scheme 是如何解決這個(gè)問(wèn)題的: 清單 14. Scheme 中的值交換
;;Define SWAP to be a macro
(define-syntax SWAP
;;We are using the syntax-rules method of macro-building
(syntax-rules ()
;;Rule Group
(
;;This is the pattern we are matching
(SWAP a b)
;;This is what we want it to transform into
(let (
(c b))
(set! b a)
(set! a c)))))
(define first 2)
(define second 9)
(SWAP first second)
(display "first is: ")
(display first)
(newline)
(display "second is: ")
(display second)
(newline)
|
這是一個(gè) syntax-rules 宏。Scheme 有幾個(gè)宏系統(tǒng),但是 syntax-rules 是其中最標(biāo)準(zhǔn)的。
在 syntax-rules 宏中,define-syntax 是用來(lái)定義宏轉(zhuǎn)換的關(guān)鍵字。在 define-syntax 關(guān)鍵字之后是要定義的宏的名字;之后是要轉(zhuǎn)換的內(nèi)容。
syntax-rules 是要采用的轉(zhuǎn)換類型。在圓括號(hào)中的是正在使用的其他符號(hào),而不是宏名本身(在這個(gè)例子中沒(méi)有宏名)。
之后是一系列轉(zhuǎn)換規(guī)則。這種語(yǔ)法轉(zhuǎn)換器會(huì)遍歷每條規(guī)則,并試圖查找一個(gè)匹配的模式。在找到這樣一個(gè)模式之后,就執(zhí)行指定的轉(zhuǎn)換操作。在這個(gè)例子中,只有一個(gè)模式:(SWAP a b)。a 和 b 是 模式變量(pattern variable),它們與宏調(diào)用中的代碼單元進(jìn)行匹配,并且用來(lái)重新安排轉(zhuǎn)換過(guò)程中的部分。
表面上來(lái)看,這與 C 版本的程序具有同樣的缺陷;然而實(shí)際上它們之間存在很多不同之處。首先,由于這個(gè)宏采用的是 Scheme 語(yǔ)言,因此類型都已經(jīng)被綁定到值本身上面了,而不是綁定到變量名上面,因此根本不用擔(dān)心會(huì)出現(xiàn) C 版本中那種變量類型的問(wèn)題。但是它是否也有原來(lái)的變量名問(wèn)題呢?如果一個(gè)變量被命名為 c,那么這不會(huì)產(chǎn)生沖突嗎?
實(shí)際上的確不會(huì)。Scheme 中使用 syntax-rules 的宏都是 hygienic。這意味著宏所使用的所有臨時(shí)變量都會(huì)在 替換發(fā)生之前 自動(dòng)重新進(jìn)行命名,從而防止名字產(chǎn)生沖突。因此在這個(gè)宏中,如果替換變量是 c,那么在替換發(fā)生之前 c 就會(huì)被重新命名成其他的名字。實(shí)際上,此時(shí)通常都會(huì)重新進(jìn)行命名。清單 15 是對(duì)這個(gè)程序進(jìn)行宏轉(zhuǎn)換的一種可能的結(jié)果: 清單 15. 值交換宏的一種可能轉(zhuǎn)換結(jié)果
(define first 2)
(define second 9)
(let
(
(__generated_symbol_1 second))
(set! second first)
(set! first __generated_symbol_1))
(display "first is: ")
(display first)
(newline)
(display "second is: ")
(display second)
(newline)
|
正如您可以看到的一樣,Scheme 的宏可以提供其他宏系統(tǒng)的優(yōu)點(diǎn),卻沒(méi)有那些系統(tǒng)的問(wèn)題。
然而,有時(shí)您可能會(huì)希望宏不是 hygienic 的。例如,可能希望在那些正在轉(zhuǎn)換的代碼中綁定這個(gè)宏。簡(jiǎn)單地聲明一個(gè)變量并不能實(shí)現(xiàn)這種功能,因?yàn)?syntax-rules 系統(tǒng)會(huì)對(duì)變量重新進(jìn)行命名。因此,大部分模式還包含一個(gè)非 hygienic 的宏系統(tǒng),通常稱為 syntax-case。
syntax-case 宏很難編寫(xiě),但是其功能更加強(qiáng)大,因?yàn)檫@樣就可以使用完整的 Scheme 系統(tǒng)功能來(lái)進(jìn)行轉(zhuǎn)換了。syntax-case 宏并不是實(shí)際的標(biāo)準(zhǔn),但是它們?cè)诤芏?Scheme 系統(tǒng)中都已經(jīng)實(shí)現(xiàn)了。沒(méi)有 syntax-case 宏的系統(tǒng)通常也會(huì)有其他類似的系統(tǒng)可以使用。
讓我們來(lái)看一下 syntax-case 宏的基本格式。讓我們來(lái)定義一個(gè)宏 at-compile-time,它將在編譯時(shí)執(zhí)行一個(gè)給定的表單。 清單 16. 在編譯時(shí)生成單個(gè)值或成組值的宏
;;Define our macro
(define-syntax at-compile-time
;;x is the syntax object to be transformed
(lambda (x)
(syntax-case x ()
(
;;Pattern just like a syntax-rules pattern
(at-compile-time expression)
;;with-syntax allows us to build syntax objects
;;dynamically
(with-syntax
(
;this is the syntax object we are building
(expression-value
;after computing expression, transform it into a syntax object
(datum->syntax-object
;syntax domain
(syntax k)
;quote the value so that its a literal value
(list ‘quote
;compute the value to transform
(eval
;;convert the expression from the syntax representation
;;to a list representation
(syntax-object->datum (syntax expression))
;;environment to evaluate in
(interaction-environment)
)))))
;;Just return the generated value as the result
(syntax expression-value))))))
(define a
;;converts to 5 at compile-time
(at-compile-time (+ 2 3)))
|
它可以在編譯時(shí)執(zhí)行給定的操作。更具體地說(shuō),它是在宏展開(kāi)時(shí)執(zhí)行給定的操作,在 Scheme 系統(tǒng)中宏展開(kāi)與編譯并不總是同時(shí)進(jìn)行的。Scheme 系統(tǒng)中編譯時(shí)允許執(zhí)行的任何表達(dá)式都可以在這個(gè)表達(dá)式中使用?,F(xiàn)在讓我們來(lái)看一下這是如何工作的。
使用 syntax-case 系統(tǒng),實(shí)際上是在定義一個(gè)轉(zhuǎn)換函數(shù),這就是 lambda 發(fā)揮作用的地方。x 是正在轉(zhuǎn)換的表達(dá)式。with-syntax 額外定義了一些語(yǔ)法元素,可以在轉(zhuǎn)換表達(dá)式中使用。syntax 可以使用這些語(yǔ)法元素,并將其組合在一起,它遵循與 syntax-rules 中相同的轉(zhuǎn)換規(guī)則。讓我們來(lái)看一下每個(gè)步驟中會(huì)發(fā)生什么操作:
at-compile-time 表達(dá)式匹配。
- 在轉(zhuǎn)換最內(nèi)部的地方,
expression 被轉(zhuǎn)換成一個(gè)列表,并作為普通的模式代碼進(jìn)行分析。
- 然后,結(jié)果與符號(hào)
quote 合并到一個(gè)列表中,這樣 Scheme 就會(huì)在將其轉(zhuǎn)換成代碼時(shí)將其作為一個(gè)文本值進(jìn)行處理。
- 這些數(shù)據(jù)被轉(zhuǎn)換成一個(gè) syntax 對(duì)象。
- 這個(gè) syntax 對(duì)象在輸出結(jié)果中使用名字
expression-value 表示。
- 轉(zhuǎn)換器
(syntax expression-value) 認(rèn)為 expression-value 是這個(gè)宏的全部輸出。
利用這種在編譯時(shí)執(zhí)行計(jì)算的功能,我們可以編寫(xiě)一個(gè)比 C 語(yǔ)言更好的 TABLE 宏。清單 17 顯示了在 Scheme 中應(yīng)該如何使用 at-compile-time 宏: 清單 17. 在 Scheme 中構(gòu)建平方根表
(define sqrt-table
(at-compile-time
(list->vector
(let build
(
(val 0))
(if (> val 20)
‘()
(cons (sqrt val) (build (+ val 1))))))))
(display (vector-ref sqrt-table 5))
(newline)
|
可以通過(guò)對(duì)這個(gè)宏進(jìn)一步進(jìn)行處理生成一個(gè)用來(lái)構(gòu)建表的宏,進(jìn)一步進(jìn)行簡(jiǎn)化,這與前面的 C 語(yǔ)言版本的宏類似: 清單 18. 用來(lái)在編譯時(shí)構(gòu)建查找表的宏
(define-syntax build-compiled-table
(syntax-rules ()
(
(build-compiled-table name start end default func)
(define name
(at-compile-time
(list->vector
(let build
(
(val 0))
(if (> val end)
‘()
(if (< val start)
(cons default (build (+ val 1)))
(cons (func val) (build (+ val 1))))))))))))
(build-compiled-table sqrt-table 5 20 0.0 sqrt)
(display (vector-ref sqrt-table 5))
(newline)
|
現(xiàn)在,有了一個(gè)可以簡(jiǎn)單地構(gòu)建任何想要的表的函數(shù)。
結(jié)束語(yǔ)
我們已經(jīng)介紹了很多知識(shí),因此現(xiàn)在花一分鐘來(lái)回顧一下。首先我們討論了哪些問(wèn)題最適合使用代碼生成程序來(lái)解決。這包括以下問(wèn)題:
- 需要提前生成數(shù)據(jù)表的程序
- 有大量樣板文件的程序,但是無(wú)法抽象成函數(shù)
- 使用開(kāi)發(fā)語(yǔ)言不具備的特性的程序
然后我們介紹了幾種元編程系統(tǒng),并給出了幾個(gè)使用這些系統(tǒng)的例子。這包括通用文本替換系統(tǒng),以及領(lǐng)域特有的程序和函數(shù)生成器。然后又介紹了一個(gè)具體的構(gòu)建表的示例,并介紹了用 C 編寫(xiě)這樣一個(gè)代碼生成程序來(lái)構(gòu)建靜態(tài)表的詳細(xì)過(guò)程。
最后,我們介紹了 Scheme,并了解了它如何解決我們?cè)?C 語(yǔ)言中所面對(duì)的問(wèn)題:它使用了一些結(jié)構(gòu),而這些結(jié)構(gòu)本身就是 Scheme 語(yǔ)言的一部分。Scheme 既是一種語(yǔ)言,又是一種代碼生成語(yǔ)言。由于這些技術(shù)都已經(jīng)構(gòu)建到了語(yǔ)言本身中,因此很容易編寫(xiě)程序,并且不會(huì)碰到其他語(yǔ)言中所面臨的問(wèn)題。這樣我們就可以為 Scheme 語(yǔ)言在代碼生成器傳統(tǒng)應(yīng)用的地方簡(jiǎn)單地添加一些領(lǐng)域特有的擴(kuò)展了。
本系列文章的第 2 部分將詳細(xì)介紹如何編寫(xiě) Scheme 宏,以及如何使用這些宏來(lái)極大地簡(jiǎn)化大型編程任務(wù)。
參考資料 學(xué)習(xí)
獲得產(chǎn)品和技術(shù)
- 索取免費(fèi)的 SEK for Linux,這有兩張 DVD,包括最新的 IBM for Linux 的試用版軟件,包括 DB2?、Lotus?、Rational?、Tivoli? 和 WebSphere?。
- 在您的下一個(gè) Linux 開(kāi)發(fā)項(xiàng)目中采用 IBM 試用版軟件,這可以從 developerWorks 上直接下載。
討論
|