小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

元編程藝術(shù):程序的程序

 老莊走狗 2005-12-11

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è)首


文本宏語(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.hMakefile 文件。

這些 M4 宏處理器的詳細(xì)信息太過(guò)復(fù)雜,我們就不再在本文中進(jìn)行介紹了,不過(guò)在 參考資料 一節(jié)中給出了有關(guān) M4 宏處理器及其在 sendmail 和 autoconf 中的用法的鏈接。



回頁(yè)首


用來(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)該是:

  1. 讀入一行。
  2. 判斷該行是否應(yīng)該進(jìn)行處理?
  3. 如果應(yīng)該,就對(duì)該行進(jìn)行處理,并生成輸出結(jié)果。
  4. 否則,就簡(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)空間。



回頁(yè)首


使用 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)。ab模式變量(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ā)生什么操作:

  1. at-compile-time 表達(dá)式匹配。
  2. 在轉(zhuǎn)換最內(nèi)部的地方,expression 被轉(zhuǎn)換成一個(gè)列表,并作為普通的模式代碼進(jìn)行分析。
  3. 然后,結(jié)果與符號(hào) quote 合并到一個(gè)列表中,這樣 Scheme 就會(huì)在將其轉(zhuǎn)換成代碼時(shí)將其作為一個(gè)文本值進(jìn)行處理。
  4. 這些數(shù)據(jù)被轉(zhuǎn)換成一個(gè) syntax 對(duì)象。
  5. 這個(gè) syntax 對(duì)象在輸出結(jié)果中使用名字 expression-value 表示。
  6. 轉(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ù)。



回頁(yè)首


結(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ù)。



回頁(yè)首


參考資料

學(xué)習(xí)
  • 您可以參閱本文在 developerWorks 全球站點(diǎn)上的 英文原文 。

  • 在 “通過(guò)有效處理列表更好地實(shí)現(xiàn)編程”(developerWorks,2005 年 1 月)中,Jonathan 介紹了單鏈表和 Scheme 技術(shù)。

  • Using XML and XSL for code generation”(developerWorks,2001 年 5 月)介紹了 XSL 樣式表的一種不太常見(jiàn)的用法:通過(guò)將 XML 文檔轉(zhuǎn)換成源代碼來(lái)為常見(jiàn)的設(shè)備創(chuàng)建應(yīng)用程序。

  • Code generation using XSLT 教程(developerWorks,2003 年 4 月)對(duì)代碼生成的概念進(jìn)行了基本的介紹。

  • 用代碼生成取代反射”(developerWorks,2004 年 6 月)展示了如何使用運(yùn)行時(shí)類處理來(lái)用生成的代碼替換反射代碼。

  • GNU CPP manual 中有很多有關(guān) GCC 宏處理功能的信息,以及在編程時(shí)您需要了解的一些警告。

  • 有關(guān) M4 的更多信息,請(qǐng)參閱 manual for GNU M4

  • 如果您希望了解更多有關(guān) Sendmail 的 M4 宏的知識(shí),請(qǐng)參閱 official Sendmail M4 macro reference。

  • 要了解更多有關(guān) GNU 編譯系統(tǒng)以及如何使用 autoconf 和 M4 宏來(lái)實(shí)現(xiàn)自動(dòng)化操作的更多知識(shí),請(qǐng)參閱 GNU build system tutorial。

  • 這些 教程 可以讓您入手學(xué)習(xí)使用 syntax-casesyntax-rules 的 Scheme 宏編程。

  • Code Generation in Action (Manning,2003)介紹了構(gòu)建高質(zhì)量的機(jī)器生成的代碼所采用的技術(shù)和實(shí)現(xiàn),并給出了構(gòu)建各種代碼生成器的詳細(xì)步驟。

  • developerWorks Linux 專區(qū) 中可以找到為 Linux 開(kāi)發(fā)人員準(zhǔn)備的更多參考資料。

  • 緊密跟蹤 developerWorks 技術(shù)動(dòng)態(tài)和事件。

獲得產(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 上直接下載。



討論
  • 有關(guān)編程的一個(gè)非常有用的資源是 Code Generation Network,它為注重實(shí)效的工程師提供了代碼生成方面的信息。

  • 通過(guò)參與 developerWorks blogs 來(lái)加入 developerWorks 社區(qū)。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多