第三章 TinyOS编程语言nesC
第一节 C与nesC的比较
如果某个目标文件引用一个变量,在连接阶段,就会被连接到定义这个变量的目标文件(可能是库文件或标准的目标文件)。以函数为例,由于声明函数仅对应一个函数定义,这表明代码引用一个函数,实际是引用了它的实现。如果两个源文件引用相同的函数名字,则实际上引用的是同一个函数,这就在两块代码间引入了潜在的依赖性。因为如果想改变一个文件使用的函数实现,那么也会改变另一个源文件代码使用的函数实现。这种情况显然不是我们想要看到的。函数指针是避免这种间接绑定的一个常用办法。在代码中不引用特定的函数,而是引用一个存放函数指针的变量,这个指针可以指向任意一个函数。这就使得代码可以在运行时再解析绑定,也就是说,通过选择在变量中存放什么函数地址值(指针)确定绑定。通过使用函数指针,C程序可以调用那些在编译时还没有或不能命名的代码块。这对于具有回调(callback)的系统是非常重要的。
例如,GUI工具箱需要调用函数对用户事件进行反应,函数是应用代码的一部分,但是在GUI工具箱预编译时需要调用它。具体地说,应用创建一个按钮,给这个按钮一个函数指针,当按钮被单击时,调用函数指针所指的函数。C中没有其他方式可以高效地做到这一点,因为工具箱不能命名这个函数。所以必须使用函数指针,而该指针必须在运行时分配。
UNIX或Linux内核使用的虚拟文件系统也是一个典型的例子。操作系统具有访问任意数目的文件系统的需求,将所有文件系统静态编译到内核是很麻烦的。如果为了支持一个新的文件系统,如USB存储设备,就需要重新编译操作系统,这很烦琐,是不应该的。为了避免这一点,操作系统使用虚拟文件系统(Virtual file system,VFS)接口与文件系统进行交互。这是一套基本的操作,比如read( )和write( )等,存放在一个数据结构中。每个文件系统实现都使用自己的函数来实现这个数据结构,然后把这个数据结构传给内核。比如,当用户程序要读CD时,OS读取与CD文件相关的VFS结构,调用read( )函数指针,从而使得内核与特定的文件系统保持独立。这个基本方法与GUI工具箱相同。总之,正是由于C的命名方法,操作系统必须使用运行时分配的函数指针来达到灵活的程序连接与扩展。
C++除了具有更为丰富的命名范围和继承性之外,其他与C相似。C++使用双冒号::表明命名范围的层次性。虽然类可以在其域和方法上使用不同的访问描述符,但这与C中关键字static声明的变量不同,它不是在命名范围的级别上操作。static表明变量和函数不会出现在全局范围,因此也不会在全局范围内被命名,其他文件想要访问这样的变量,系统会报“no such variable exists”的错误。相对的,C++类中使用private指定变量为私有的,意味着如果其他代码引用私有变量,就会产生一个“access violation”的编译错误。
C++的继承性提供了比C更容易扩展功能的方式,程序使用类而不是函数指针来表示可扩展抽象。实践中,每个对象都有一个指向自己的函数表的指针。该函数表是只读的,不需要程序员维护它。基于C++的系统使用对象而非函数指针来扩展功能。与C一样,在运行时建立变量名字之间的绑定关系。
C++中,变量名字的绑定使得工厂函数(Factory)成为软件工程实践中的通用对象。工程将对象与其初始化分开。比如,Tetris 游戏可能需要创建游戏片段。一种简单的方式是让游戏引擎直接分配各个片段。将游戏引擎绑定到一个具体类比如JPiece,也同时绑定了JPiece构造函数的参数列表。这种绑定是不必要的,而且,如果更改类的名字,比如改成JTetrisPiece,会产生问题。如果不将游戏引擎绑定到一组特定类的集合,而是定义一个抽象的类Piece,仅仅要求一个工程来为它创建类,这样,游戏引擎与Piece的名字及实现就相互独立了。也就是说,改变一组Piece仅仅要求改变工程,因为仅有一个分配点,构造函数的变化都可以局部到工程中来。工程的设计模式使得命名具有一定程度的间接性,从而打断了那些不想要的特定类之间的依赖关系。
3.1.2 nesC
C与nesC之间最本质的区别是如何组织程序。下面以两个简单应用为例,结合其C实现和nesC实现进行讨论。
1.一个例子的C实现与nesC实现
例3.2:Powerup应用
C实现代码:
#include “mote.h”
main()
{
mote_init();
Led0_on();
sleep();
}
Powerup应用的C实现被编译并连接到一个“mote”库。该库提供了硬件初始化、控制二极管以及设置节点低功耗状态等函数。节点启动后,main函数自动被调用。
Powerup应用的nesC实现代码见2.4.1节,主要分为两个组成部分:一是模块PowerupC,包含应用Powerup的执行逻辑;二是配件PowerupAppC。
在模块PowerupC的代码中,可以看出模块PowerupC基于两个接口Boot和Leds与系统的其他部分(服务)交互。配件PowerupAppC指定了模块PowerupC如何连接到TinyOS的服务,显式地指定了接口之间的连接关系。MainC启动系统,它通知Boot接口的booted事件,在配件中将Boot接口连接到PowerupC,因此该事件被通知到组件PowerupC中。在这个事件的实现中调用了接口Leds的led0On命令,因此就可以打开LED0。组件组织结构可以由文档生成工具nesdoc生成,具体见2.4.2节。