《深入BREW》 - BREW原理 | Computer, Electron and Technology

44 views
Skip to first unread message

Chunlin Zhang

unread,
Jun 9, 2009, 2:51:56 AM6/9/09
to BREW 中国开发者讨论组
http://www.donevii.com/post/746.html

 
 

Sent to you by Chunlin Zhang via Google Reader:

 
 

via www.donevii.com on 6/8/09

18 Jan 09 《深入BREW》 - BREW原理

我们PC的Windows操作系统功能是如此的强大,以至于我们可以获得任何一个我们想要的程序,任何一个人都可以为Windows开发应用程序,或者给自己,或者为他人。而这一切源于操作系统的开放性和硬件平台的不断发展,尤其是存储器的发展,使得我们在编写程序的时候不必在意需要多大的存储空间了。

       然而,嵌入式系统可就没那么幸运了。至今为止,在嵌入式系统里仍然没有一个能够像Windows这样应用如此广泛的操作系统,也没有可以不考虑存储空间的硬件平台。在数以亿计的嵌入式设备使用者中,都还在用着一成不变的应用程序,单调同时也令人乏味。我们能不能也像在Windows下面一样,在嵌入式系统中可以安装应用程序呢?应该怎样克服嵌入式系统的限制而实现这个功能呢?
       有梦想才会不断的追求!我们知道,在Windows中程序都是以文件的形式存储在文件系统中的,然后通过操作系统控制这些程序的运行,我们可以说它的程序是“分散式”的。而在嵌入式系统中通常是将程序烧录在一个Flash芯片中,文件系统在另一个Flash芯片中(也可以二者在同一个芯片中),CPU是直接从程序Flash芯片中读取指令执行的,没有经过文件系统,我们可以叫这种程序是“一体式”的。Windows的“分散式”程序体通过文件的形式存在,可以把程序的不同部分分割成不同的文件,当我们只需要更新一个模块内容的时候,只更新这个文件就可以了。熟悉它的朋友们可能已经知道了,这个文件就是在Windows操作系统中的DLL文件。这样的方式可以很容易的实现程序分发,这给了我们一个很好的启示:嵌入式系统中也有文件系统,把程序放在文件系统里不就可以了吗?真是个好主意!
       在我们庆幸找到了好方法的时候,问题不偏不倚地出现了:系统如何运行文件系统中的程序,文件中的程序又如何调用平台中的函数?要实现“分散式”的程序运行,这两个问题是必须要解决的,而其中第二个问题就更为重要了。或许您现在还不是十分的明白这些问题的意义,不要着急,这一章里我会逐一的向您讲解如何理解并解决这两个问题。当然,现在我们知道BREW已经在嵌入式系统中解决了这两个问题,从现在开始就让我们沿着开发者的足迹去追寻BREW的本质吧。
1.平台的作用
       如果想要清楚的了解我们在嵌入式系统中所面临的问题,那么我们就首先需要了解“分散式”系统的结构。一个“分散式”系统需要有三个部分组成:平台、软件开发工具包(SDK: Software Development Kit)和应用程序。“分散式”应用程序的运行需要平台的支持,就像是DLL文件只有在Windows操作系统平台下才有作用,而到了Linux平台则不起任何作用一样;应用程序则通过SDK进行开发,开发出的源程序经过编译之后可以运行在运行平台之上。平台又分为开发平台和运行平台,开发平台是SDK运行的平台,用来开发可以在运行平台上运行的应用程序,对于一些系统还会提供模拟运行平台的模拟器,以便于在没有显而易见的运行设备的时候也可以看到开发的效果;运行平台是应用程序运行的平台,它提供应用程序运行的环境,同时肩负着控制应用程序的作用。开发平台和运行平台可以是同一个平台,也可以两个不同的平台,比如现在的Winows平台的应用就可以使用VC等工具开发基于Windows的应用程序,而BREW SDK则是运行在Windows环境下,但应用程序却在嵌入式系统中运行。它们之间的关系如下图:

 

图1 分散式系统结构图
       从这个图中我们可以看出,SDK需要使用运行平台的接口声明来开发应用程序,运行平台负责根据用户的输入启动应用程序,而应用程序则通过运行平台的接口调用运行平台的函数库来实现功能,我们的问题主要集中在应用程序和运行平台的互动关系上。
       从前面的编译器基础一章中我们可以知道,在固定链接的模式下,各个函数的地址是固定的,我们可以在同一个映像文件中调用任何函数。而存储在文件系统中的程序就不一样了,文件系统中的程序没有固定的位置并且地址也不连续,我们该怎样实现应用程序的启动呢?可选的方案就是将应用程序复制到一个连续的内存块中去,然后在内存中执行程序。在这里需要特别的说明一下,在PC的Windows操作系统中,Windows将全部的程序载入内存中运行,并且其中包含了复杂的内存管理功能,但是在嵌入式系统中通常程序是在Flash芯片中运行的,只有可读写的数据是放在RAM中的,具体的细节可以参考编译器基础一章。BREW主要是应用程序在嵌入式系统中的,因此将程序复制到内存中执行是需要特殊处理的。这个特殊的处理就是我们所面临的第一个问题了——系统如何运行文件系统中的程序。
       在我一开始理解BREW的时候,我曾经认为系统如何运行存在于文件系统中的程序是我们所面临的主要问题,但当深入BREW内部的时候发现根本不是这么回事。现在我们可以假设运行平台分配了一个足够大的内存,这块内存地址是已知的,可以想象的到我们可以从这个地址开始执行程序。现在我们暂时忽略那些特殊的处理,文件系统中的这个程序现在正在运行,就像在Flash中的程序一样的在运行。从理论上来讲这个是行得通的,因此系统如何运行文件系统中的程序的问题并不是我们所面临的难题。实际上BREW也是按照这个思路做的,只是实现时还有细节的东西在,关于这些细节我们将在Shell内幕一章进行详细的介绍。
       进一步的,假设现在程序运行到了需要调用平台函数的时候了,问题就来了,由于当前的应用程序是开发者使用SDK开发的,就像平台不知道应用程序的地址一样,应用程序也不知道平台的函数地址,因此,我们现在面临的问题是怎么能够知道应用程序中所调用的平台函数的地址。SDK可以提供运行平台中的每个函数的地址吗?可以提供,但是行不通。因为平台是会经常升级的,导致每个函数的链接地址不固定,如果由SDK提供所有函数的地址带来的问题是,只要运行平台一升级,那么SDK和应用程序都需要同时升级。如果这样的话我们的分散式程序就不能实现“分散式”的升级了,这种程序的运行方式也就没有任何意义了。看来我们还要寻找更为高级的方法,这种方法要能够提供一种应用程序与运行平台之间无关的机制。我们现在所需要的这个“机制”就是第二个问题了——文件系统中的程序如何调用平台的函数。
       从分散式系统结构图中我们可以看到,SDK使用的是运行平台的接口声明,应用程序调用真正的运行平台接口。或者换句话说开发过程中使用运行平台的接口声明,而在运行时应用程序使用真正的二进制接口,并且在二进制层面调用接口函数。那么,现在无论是SDK还是应用程序都与接口相关,那么,可以想象的到的解决第二个问题的方式就是让接口和接口实现之间分离。接口与实现间分离的方法就是BREW的核心,也是接下来我们主要阐述的议题。
2.软件分发和C语言
       为了更好的理解“实现接口和实现分离”所面临的问题,让我们先来看看通常的C语言软件库是如何分发的,这对于我们的理解非常有用。为了能够更加清楚地理解问题,我们在接下来几节的论述中不会仅仅局限于嵌入式系统,因为同样的问题也存在于PC系统中,更为重要的是PC系统所面临的问题更加典型,并且这些问题在软件系统中是具有普遍性的。
       想象现在有一个C语言库的开发厂商,它开发了一个算法,可以在O(1)时间效率内实现子字符串的搜索,O(1)时间效率的意思是指搜索时间为常数,与目标字符串的长度没有关系。为了实现这个功能,软件厂商生成了一个头文件FastString.h,包含如下内容:
FastString.h接口声明文件第一版
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
 
// 要求使用者不能直接使用此结构体中的内容
typedef struct _IFastString {
char *m_pString;   // 指向字符串的指针
} IFastString;
 
// 创建目标字符串对象
void IFastString_CreateObject(IFastString * pIFastString, char *pStr);
 
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString);
 
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString);
 
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr);
#endif // FASTSTRING_H_
       除了这个头文件之外软件厂商还提供了接口的实现文件FastString.c
FastString.c接口实现文件第一版
#include ”FastString.h”
#include <string.h>
 
// 创建目标字符串对象
void IFastString_ CreateObject (IFastString * pIFastString, char *pStr)
{
IFastString *pMe = pIFastString;
 
if(pMe == NULL pStr == NULL)
{
    return;
}
 
pMe->m_pString = malloc(strlen(pStr) +1);
strcpy(pMe->m_pString,pStr);
}
 
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString)
{
    IFastString *pMe = pIFastString;
   
if(pMe == NULL)
{
    return;
}
 
if(pMe->m_pString)
{
    free(pMe->m_pString);
}
}
 
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString)
{
IFastString *pMe = pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
    return strlen(pMe->m_pString);
}
 
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
IFastString *pMe = pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
       这个接口总共有四个接口:CreateObject、Release、GetLength和Find。CreateObject用来创建IFastString接口,从实现中我们可以看到它构建了IFastString结构体的内容。Release用来释放由CreatObject分配的内存。GetLength获得字符串的长度。Find用来在目标字符串中查找指定的字符串。在这个接口的实现中,使用了一个初始化的技巧(CreateObject和Release),目的是为了再使用前构建IFastString结构体,使用后可以通过Release释放构建时分配的内存。
       一般来讲这个库的使用者会将.lib库链接到自己的工程中,通过接口声明的头文件使用库中的函数,这是一个非常可行的做法。这样做带来的结果是库的可执行代码将成为客户应应用程序中不可分割的一部分。
       现在假设对于FastString库文件占用了大约16M的代码空间(这里假设为了完成O(1)算法可能需要十分复杂的程序,并且采取了一些空间换时间的策略等,这可能占用大量的存储空间)。如图2所示,如果现在有三个应用程序都在使用FastString库,那么每一个应用程序都将使用16M的空间来存储这些代码,总共花费了48M的空间。如果一个用户安装了这三个程序,也就是说有32M的空间浪费了,去存储了同样的FastString.lib中的代码。
 
                                                        图2 多个应用程序使用FastString库
       在这种情况下的另一个问题是,如果当前FastString库的厂商发现了程序中的缺陷,那么就没有任何办法可以替换已经存在的缺陷代码。一旦FastString的代码链接到了应用程序中,我们就不可能在用户的设备上替换这部分的代码。因此,库厂商不得不重新为每个应用程序的开发者广播发布新的库文件,并且希望他们能够重新的编译他们的应用程序,以便能够使用新的代码。很显然,这可真是一件麻烦的事情,一旦应用程序开发者链接了这个库文件,FastString便失去了模块化的特征。跟进一步说,这在嵌入式系统中是完全不可能的,在这里FastString的角色就是运行平台,我们总不能每一个应用程序都包含一个运行平台去啊。
3.动态链接
       解决上面问题的一种技术是使用动态链接技术(Dynamic Link)将FastString包含起来,这种技术的典型应用是Windows操作系统中的动态链接库(DLL文件)。这种方法是将FastString源文件编译成特殊的独立的二进制文件,并强迫FastString将所有的接口从二进制文件中引用出去,建立相应的引出表,以便于在运行时把每个接口的名字映射到对应的二进制接口地址上。与此同时还需要为使用者生成相应的引入库,通过这个引入库FastString的使用者可以获得FastString中每个接口的符号。引入库中并没有包含实际的代码,它只是简单的包含FastString引出的符号名字。当客户链接引入库的时候,这些符号信息会加入到当前的可执行文件中,运行时动态的装载FastString二进制库文件,并在执行时调用相应的程序。当然这些需要一些辅助工具的支持,例如编译器的支持等。此时应用程序的结构如图3:
                                                                            图3 动态链接示意图
       上图就是FastString在动态链接方式下的运行模式,这里面的引入库非常的小,所以可以忽略它占用的空间。在这种情况下,FastString的代码库就只需要一份了。运行时所有的应用程序调用同一个库中的内容,理论上当发现FastString中有缺陷的时候,我们可以更新FastString二进制组件而不影响应用程序。可以看到,我们已经迈出了重要一步,不过还没有完全解决我们所面临的问题。
4.封装性
       我们现在已经找到了一种可以实现动态链接的方法,那么下一个问题则与封装有关。考虑这样的情形:一个组织使用了FastString,同时希望能够在2个月内完成开发和测试。假设在这两个月中,某些具有特殊的怀疑精神的开发人员打算在他们的应用程序上测试一下FastString的性能,以便于测试O(1)时间效率的搜索算法。令人惊讶的是Find方法的搜索速度很快,并且与字符串的长度无关,但是GetLength方法的速度不是很理想,因为在GetLength方法中使用的是strlen计算字符串的长度,它查找字符串中的NULL结束符,它的算法需要遍历正个字符串的内容,因此它的执行效率是O(n),当字符串很长,而且调用次数很多的时候,执行的速度很慢。于是开发人员要求厂商提高GetLength操作的执行速度,使它在常数时间内完成。但是现在有一个问题,开发人员已经开发完成了他们的应用程序,他们不希望由于使用新的GetLength方法而更改任何现有的程序。而且其他的厂商可能已经发布了使用这个现有版本的基于FastString的产品,从任何方面将库厂商都不应该影响这些已经面世的产品。
       这个时候我们要查看我们的FastString的实现,以便确定哪些可以改变,哪些不可以改变。幸运的是,我们已经要求使用者不能直接使用IFastString结构体中的内容,假设所有的使用者都遵循了这一约定,于是我们很快的修改了GetLength的方法,将头文件改成了如下的方式(未修改部分未列出):
FastString.h接口声明文件第二版
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
 
// 要求使用者不能直接使用此结构体中的内容
typedef struct _IFastString {
char *m_pString;   // 指向字符串的指针
int m_nLen;       // 存储字符串的长度
} IFastString;
 
#endif // FASTSTRING_H_
将实现文件改成了如下方式(未修改部分未列出):
FastString.c接口实现文件第二版
// 创建目标字符串对象
void IFastString_ CreateObject (IFastString * pIFastString, char *pStr)
{
IFastString *pMe = pIFastString;
 
if(pMe == NULL pStr == NULL)
{
    return;
}
 
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
}
 
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString)
{
IFastString *pMe = pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
    return pMe->m_nLen;
}
       很快的修改后重新发布了FastString的第二个版本。在这里一个显著的改进是在CreateObject时将字符串长度存储起来了,当用户调用GetLength方法时直接返回存储的长度。这样没有修改任何接口的内容,因此应用程序也就不需要修改了。
       客户收到了第二版的FastString后,替换了FastString的动态链接库,重新编译链接全部的应用程序,测试后发现不但原始代码不需要任何修改,而且GetLength方法的速度也大大加快了。最终这个第二版的FastString会随着这个产品而发布到用户手中。在安装应用程序的时候,第二版的FastString会将第一版的替换掉。这通常不会有问题,因为修改并没有影响公开的接口,因该只会增强原先已经安装的使用FastString的应用程序的功能而以。
       请想象这样的情形,当用户更新了第二版的FastString后,运行新版的应用程序,用户惊喜的发现程序运行的速度提高了。然后用户关闭了新的应用程序,而打开了另一个以前安装的使用旧版本FastSting的应用程序。现在的FastString已经替换成了第二版,因此用户发现这个应用程序的性能也增强了。然而不久异常出现了,系统出现了未知的错误。不过没关系,对于已经习惯了现代商业软件的人士来说,这不算什么问题,于是重新卸载并重新安装了两个应用程序,还是不起作用啊,异常依然发生!到底是怎么回事?
       原因在于IFastString结构体的修改。在未修改前sizeof(IFastString) == 4,因为只有一个char *m_ pString变量(假设系统是32位的)。修改后sizeof(IFastString) == 8,增加了4个字节。新版本的软件已经重新编译了,因此相应的IFastString结构体已经增加了空间。但是对于使用第一版FastString编译的应用程序来说,此时在应用程序里面分配的IFastString的空间依然是4,于是当第一版的应用程序调用第二版FastString的时候,将本该属于其他用途的4个字节用作了m_nLen的区间,这是十分粗暴的,产生异常也就不足为奇了。
       还记得前面的约定吗?我们要求FastString的使用者不可以直接对IFastString结构体中的数据进行直接操作,以达到应用程序与数据结构间的无关性。但实际上这样的约定基本上不可能被遵守,因为在实际中不管出于什么样的目的,总会有些开发者直接使用结构体中的内容(这些开发者使用的使FastString的C语言库文件形式,没有使用动态链接技术)。加上前面的异常,这一切的根源是我们没有一个实现二进制数据封装的方式。如果现在能够有一种可以将全部的数据结构封装在FastString内部的方法就好了。C语言是灵活的,只要我们找到了问题,我们就可以实现它。于是第三版的FastString新鲜出炉了,首先是头文件:
FastString.h接口声明文件第三版
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
 
typedef void IFastString;
 
// 创建目标字符串对象
void IFastString_CreateObject(IFastString ** ppIFastString, char *pStr);
 
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString);
 
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString);
 
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr);
#endif // FASTSTRING_H_
接下来是实现的源文件:
FastString.c接口实现文件第三版
#include ”FastString.h”
#include <string.h>
 
typedef struct _CFastString {
char *m_pString;   // 指向字符串的指针
int m_nLen;       // 存储字符串的长度
} CFastString;
 
// 创建目标字符串对象
void IFastString_CreateObject (IFastString **ppIFastString, char *pStr)
{
CFastString *pMe = malloc(sizeof(CFastString));
 
if(pMe == NULL pStr == NULL)
{
    return;
}
 
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
 
* ppIFastString = (IFastString *)pMe;
}
 
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString)
{
    CFastString *pMe = (CFastString *)pIFastString;
   
if(pMe == NULL)
{
    return;
}
 
if(pMe->m_pString)
{
    free(pMe->m_pString);
}
 
free(pMe)
}
 
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
    return strlen(pMe->m_pString);
}
 
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
CFastString *pMe = (CFastString *)pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
       在这个实现中,我们使用了CFastString做为FastString的内部数据结构,定义了void型的IFastString类型做为接口指针传递,还有重要的一条是通过CreateObject获得数据结构的存储空间。通过这样的实现方法,我们将全部的数据类型封装在了FastString库中,这样,无论新的还是老的应用程序,使用的都是统一的IFastString指针,真正的数据是在CreateObject中进行创建的,也就不会出现上面的两种情况了。对于FastString的客户来说,他们所能看到的就是IFastString的void类型和四个接口函数,内部的CFastString的结构已经被隐藏起来了。不过这个第三版的FastString修改了接口函数CreateObject,因此不能够与前两版兼容。不过不要紧,我们现在只是在说明一个更好的方法而已。
4.虚拟函数表
       封装性的本质是实现了接口与实现之间在二进制层次的分离,第三版的FastString似乎已经解决了我们所面临的第二个问题。不过现在我们的接口仍然在使用着动态链接用的引入库文件,而且相应的动态链接库也需要提供由符号名到二进制函数地址映射的内容,为了支持这些特性,我们必须要修改相应的编译器才行。修改编译器,很复杂不是吗?
       让我们再回到嵌入式系统上来吧,ARM CPU是在嵌入式系统中使用最广泛的CPU,因此相应的ARM编译器也是应用最广泛的,基本上成为了一种通用的编译器。我们怎么修改这个编译器呢?似乎难度有点大。更近一步的,嵌入式系统中大大小小的CPU有好多种,我们也不可能把所有的这些编译器都修改了啊,看来修改编译器的可能性不大。
       又一次让我们体验到了理想与现实之间的差距!不过别灰心,看看我们现在的接口,它是二进制层面上的,我们能不能把这个引入库变成标准的C语言的头文件同时又能够实现接口与实现间的分离呢?如果真的能够实现这样的方法,那么将意味着我们可以通过通用编译器来实现动态链接的技术。这可真是令人兴奋,这个方法要比动态链接技术还要好。
       按照这个思路进一步分析,如果将现在的第三版FastString使用的C头文件做为标准的接口文件,那么将意味着各个接口需要静态的链接到应用程序中,接口和实现之间还是没有实现分离,难道我们转了一圈又回到原点了?真的又回来了,不过,不同的是我们现在已经得到了第三版的FastString。现在我们已经知道了通过CreateObject来获得内部的空间,那么我们是否也可以通过一个函数来获得接口呢?可以,这个技术就是虚拟函数表(VTBL)。
       这可真是“山重水复疑无路,柳暗花明又一村”啊,先看看我们这个第四版的FastString的头文件吧:
FastString.h接口声明文件第四版
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
 
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
typedef void (*PFNCreateObject)( IFastString **ppIFastString, char *pStr);
 
struct _IFastString
{
 struct IFastStringVtbl *pvt;
};
 
struct _IFastStringVtbl
{
void (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
};
 
// 释放目标字符串对象
#define IFASTSTRING_Release(p) ((IFastString*)p->pvt)->Release(p)
 
// 获取目标字符串长度
#define IFASTSTRING_GetLength(p) ((IFastString*)p->pvt)->GetLength(p)
 
// 查找字符串,返回偏移量
#define IFASTSTRING_Find(p,s) ((IFastString*)p->pvt)->Find(p,s)
#endif // FASTSTRING_H_
       接下来是C语言的源文件:
FastString.c接口实现文件第四版
#include ”FastString.h”
#include <string.h>
 
typedef struct _CFastString {
    IFastStringVtbl *pvt; // 指向虚拟函数表的指针
char *m_pString;    // 指向字符串的指针
int m_nLen;        // 存储字符串的长度
} CFastString;
 
// 函数声明
static void IFastString_Release(IFastString *pIFastString);
static int IFastString_GetLength(IFastString *pIFastString)
static int IFastString_Find(IFastString *pIFastString, char *pFindStr);
 
IFastStringVtbl gvtFastString = { IFastString_Release,
                           IFastString_GetLength,
                           IFastString_Find
                          };
 
// 创建目标字符串对象
void IFastString_CreateObject (IFastString **ppIFastString, char *pStr)
{
CFastString *pMe = malloc(sizeof(CFastString));
 
if(pMe == NULL pStr == NULL)
{
    return;
}
 
pMe->pvt = &gvtFastString;
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
 
* ppIFastString = (IFastString *)pMe;
}
 
// 释放目标字符串对象
static void IFastString_Release(IFastString *pIFastString)
{
    CFastString *pMe = (CFastString *)pIFastString;
   
if(pMe == NULL)
{
    return;
}
 
if(pMe->m_pString)
{
    free(pMe->m_pString);
}
 
free(pMe)
}
 
// 获取目标字符串长度
static int IFastString_GetLength(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
    return strlen(pMe->m_pString);
}
 
// 查找字符串,返回偏移量
static int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
CFastString *pMe = (CFastString *)pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
       首先对这个第四版的FastString程序做一个说明。在FastString头文件中我们定义了两个类型:IFastString和IFastStringVtbl。IFastStringVtbl类型是虚拟函数表的类型,IFastString中包含了指向虚拟函数表的指针。在接口定义的时候,我们使用了((IFastString*)p->pvt)来调用虚拟函数表中的函数指针,这也说明了如果要使用接口,必须要提供IFastString的指针类型。可以看出Release、GetLength和Find已经实现了在C语言定义的接口与实现函数之间的分离。
       接着看一下源文件中的情况。CFastString结构体与第三版的FastString不同的是增加了一个IFastStringVtbl类型的指针,而且这个指针在结构体的最顶部,如果将IFastString和CFastString对比一下我们可以发现他们都在顶部包含了IFastStringVtbl的指针,这就意味着CFastString是IFastString的超集。这一点是很重要的,我们可以看见在CreateObject函数中返回的IFastString指针其实是指向CFastString的指针的,只有CFastString是IFastString的超集的时候我们才可以这么做。在这个源文件中还定义了一个IFastStringVtbl的变量gvtFastString,并为这个变量初始化成了各个对应的函数,这个变量就使我们的虚拟函数表。虚拟函数表的示意图如下:
图4 虚拟函数表
       在这个第四版的FastString中,我们可以看到,除了CreateObject成员之外,其余的三个成员函数都已经添加到了虚拟函数表中,而且这个虚拟函数表还可以随着需求的增加而进行无限的扩大,我们只付出了一个函数CreateObject的代价就实现了无限多个接口和实现之间的分离了。
       由于用户需要使用CreateObject来获得IFastString的指针,因此我们没有办法将其与实现分离开,怎么办?现在只有这一根“线”还在困扰着我们,我们难道要功败垂成了吗?当然不能了。开动脑筋,回到我们应用程序的启动过程,对于一个程序,不管是由main函数或者其他的什么函数做为启动函数,都允许启动的时候传递参数,可能您已经想到了吧,我们把这个CreateObject函数做为参数传递给应用程序不就可以了吗?恍然大悟!这也就是为什么我们将CreateObject函数定义成了一个PFNCreateObject的函数类型的原因了,目的就是为了让使用者可以在应用程序中定义这种类型的函数指针。
       现在我们的应用程序、接口和实现之间已经分离了,中间使用了CreateObject这根细线连接了起来,只要接口不变,应用程序和实现之间就不会有任何的联系,包括二进制层面和C语言层面的。只不过这要求我们应用程序和接口的实现之间需要使用同一种编译器,或许这就叫做有得有失吧,不过对于嵌入式系统来说这是必须的,因为没有哪一种编译器可以支持全部的嵌入式CPU。
5.支持多个接口
       到现在为止所展示的技术已经解决了我们所面临的问题,不过对于一个平台来说不可能只有一个FastString接口,可能还有诸如FastNumber的接口。我们总不能把FastString和FastNumber两个接口的CreateObject都做为参数传递给应用程序的启动函数吧?看来我们现有的FastString需要进行一些扩展,来实现只传递一个参数给应用程序就可以创建多个接口的功能。在这里我们将增加一个叫做Shell的接口来管理其他的接口,相关的代码如下:
shell.h接口声明文件
#ifndef SHELL_H_
#define SHELL_H_
 
#define CLASSID_FASTSTRING 0×00000001
#define CLASSID_FASTNUMBER 0×00000002
 
typedef struct _IShell IShell;
typedef struct _IShellVtbl IShellVtbl;
 
struct _IShell
{
 struct IShellVtbl *pvt;
};
 
struct _IShellVtbl
{
void (*CreateInstance) (IShell *pIShell,
                    int nClassID,
                    void **ppObj,
                    unsigned int nUserData);
};
 
// 释放目标字符串对象
#define ISHELL_CreateInstance(p,c,pp,u) ((IShell*)p->pvt)->CreateInstance(p,c,pp,u)
#endif // SHELL_H_
源文件如下:
Shell.c接口实现文件
#include ”Shell.h”
 
typedef struct _CShell {
    IShellVtbl *pvt; // 指向虚拟函数表的指针
} CShell;
 
// 函数声明
static void IShell_CreateInstance(IShell *pIShell,
                           int nClassID,
                           void **ppObj,
                           unsigned int nUserData);
 
IShellVtbl gvtShell = { IShell_CreateInstance };
 
// 创建Shell对象
void IShell_CreateObject (void**ppObj, unsigned int nUserData)
{
CShell *pMe = malloc(sizeof(CShell));
 
if(pMe == NULL)
{
    return;
}
 
pMe->pvt = &gvtShell;
 
* ppObj = (void*)pMe;
}
 
// 创建由ClsID指定的
static void IShell_CreateInstance(IShell *pIShell,
                           int nClassID,
                           void **ppObj,
                           unsigned int nUserData)
{
    CShell *pMe = (CShell *)pIShell;
   
if(pMe == NULL)
{
    return;
}
 
switch(nClassID){
    case CLASSID_FASTSTRING:
        break;
 
    case CLASSID_FASTNUMBER:
        IFastNumber_CreateObject((IFastNumber **)ppObj, nUserData);
        break;
 
    default:
        break;
}
}
       在这个Shell接口中,我们定义了一个接口函数CreateInstance。它的作用是通过参数nClassID来创建指定的接口实例。IShell_CreateObject函数用来创建Shell接口本身,在使用的时候必须由系统直接调用IShell_CreateObject来产生Shell对象,然后再通过Shell接口来创建其他的接口(如FastString)。同时在这里包含了一个在本书中尚未实现的接口FastNumber,使用它仅仅是为了方便举例而已,因此有兴趣的读者可以仿照FastString接口实现FastNumber接口。
       从shell.h和shell.c文件中可以看到,Shell接口的实现方式与第四版的FastString实现方式是相同的。更进一步的,从CreateInstance接口函数的内部实现我们可以知道,它使用了一个Class ID来识别用户创建的是哪一个接口,并通过switch语句实现相关接口的CreateObject函数的调用。通过这样的Shell管理,应用程序只需要知道一个Shell接口的指针就可以创建其他的接口了。换句话说,在启动应用程序的时候,我们先调用IShell_CreateObject函数创建一个Shell指针,并将这个Shell指针做为参数传递给应用程序的启动函数,那么理所当然的,我们就可以在应用程序中使用Shell的接口ISHELL_CreateInstance来创建其他的接口了。通过这样的方式,我们不但可以实现接口的管理工作,而且同时也为接口的扩展性提供了足够的灵活性。
6.接口的扩展性
       到现在为止所展示的技术使得用户可以通过调用统一的C语言声明的接口,实现动态的二进制库的装载,这样可以无限制的升级库程序而不影响已有的应用程序,并且客户也不需要重新编译他们基于当前库文件所开发的应用程序,这对于创建一个复杂的运行平台来说是非常有用的。然而,这个接口却不能够随着时间而变化。这是因为客户在编译的时候需要有精确的接口定义,对接口的任何变化都需要客户重新编译他们的应用程序,以适应这种变化。更为糟糕的是,改变接口的定义完全违背了我们对接口封装性的要求。即便是最无伤大雅的变化,比如修改了接口的用途但是保留接口函数的原形不变,也会导致应用程序不再发生作用。这意味着接口的定义绝对不能改变,它既是语义上的约定,同时也是二进制层次上的约定。为了拥有一个稳定的,行为可预测性的运行时环境,接口的不变性这一要求是十分重要的。
       尽管接口具有不变性的原则,但是我们通常需要在一个接口定义好之后,希望能够加入原先设计时没有预测到的新功能。此时我们可以利用对虚拟函数表布局结构的知识,只是简单的把新的方法追加到现有接口的底部,就可以实现对接口的扩展。考虑下面的FastString接口声明:
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
 
struct _IFastString
{
 struct IFastStringVtbl *pvt;
};
 
struct _IFastStringVtbl
{
void (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
};
       简单的修改接口vtbl的声明,在现有的结构体内增加新的接口函数的类型声明,这样得到的二进制声明结构是原有的声明的超集,因为新的方法总是出现在旧版本方法之后。在针对新方法的接口实现中,我们可以填充在这个结构体中的函数指针:
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
 
struct _IFastString
{
 struct IFastStringVtbl *pvt;
};
 
struct _IFastStringVtbl
{
    // 第一版接口
void (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
// 第二版接口
int (*FindN)( IFastString *pIFastString, char *pFindStr, int n);
};
       这种方式完全可以正常工作。在第一版接口上开发的应用程序将忽略前三个接口之外的其他接口的信息。当老的应用程序使用第二版接口实现的二进制程序的时候,它仍然可以正常的工作。然而,新的客户总是希望可以使用新的方法FindN,以便于能够获得子字符串第N次出现的位置。如果此时应用程序的运行平台依然使用的是第一个版本的实现,那么不幸的,问题发生了。当调用一个未曾实现的接口时,显而易见的,程序崩溃了。
       这项技术的问题是修改了公开的接口,从而影响了接口的封装性。就像是只修改了C语言函数的声明会产生编译错误一样,改变了二进制的接口结构也会引起运行时的代码错误。这意味着接口必须是不可改变的,一旦公开后就不能再变化。解决这个问题有两种方法:一是允许接口的实现暴露多个接口,或者换句话说就是如果需要扩展老的接口,那么就重新定义一组新的接口,这样的话就可以在不支持新接口的旧平台上运行的时候判断接口的有效性,当然,这要求应用程序要对创建接口时的异常进行处理;二是支持在运行时对接口版本的判断或者在应用程序和运行平台之间有一种版本的比较机制,比如通过提供一个可以获得接口版本的API来进行版本的比较工作。不管是使用哪一种方法,无疑都会增加开发的负担,包括接口的开发者和用户的开发者,因此最好的方法是尽可能的不要去修改已经定义好的接口。
7.资源管理
       当我们在使用我们的接口的时候,我们还会遇到另外的一个问题,请看下面的代码:
IFastString *pFastString;
char TargetStr[] = “This is a test example only!”’
 
ISHELL_CreateInstance(pShell, CLASSID_FASTSTRING, (void **)&pFastString, (unsigned int)TargetStr);
 
(void)FastString_Test(pFastString);
       相应的FastString_Test函数如下:
int FastString_Test(IFastString *pFastString)
{
    int nOffset;
nOffset = IFASTSTRING_Find(pFastString, ”test”);
 
IFASTSTRING_Release(pFastString);
return nOffset;
}
       在这个例子里,pFastString在主函数中创建,同时做为参数传递给了FastString_Test函数,然后释放pFastString,这很正常,没有什么问题。在FastString_Test函数中调用了Find方法,并且在使用函数退出的时候释放了pFastString指针,这很正常,也没有什么问题。然而,当我们将两者结合起来的时候,问题出现了:主函数中释放了一次接口指针,而在FastString_Test函数中也释放了一次同样的接口指针。换句话说,在这个程序中由于开发者的疏忽,对同一个接口释放了两次,这将导致不可预测的异常发生。这里面的问题是在增加了对接口指针的引用的时候,没有相应的处理机制来记录当前实例引用的次数,也就是当前接口实例的一个资源管理的问题。
       或许您可以说这个问题可以通过使用者的细心来避免,那么我们再来看另外一个问题:如果当前的接口是可以共用的一些函数,比如这个接口中的方法全部是诸如STRLEN之类的重定义助手函数接口,并且在创建接口的时候需要分配一些公用的内存空间,那么,我们在每一次创建这个接口实例的时候,都必须分配不一样的存储空间吗?如果我们这样做了,实际上不会有什么问题,但是会浪费存储空间,因为他们本来是可以全部共用的。况且,如果我们全部都使用同一个指针在各个函数之间调用,那么对于这个指针来说使用起来会很危险的,因为我们不知道每一个函数是怎样处理这个指针的,这可真是太糟糕了。
       为了解决这个问题,一个可行的做法是为每一个接口增加一个引用计数的管理机制。在增加了这个机制之后的第五版FastString实现如下:
FastString.h接口声明文件第五版
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
 
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
 
struct _IFastString
{
 struct IFastStringVtbl *pvt;
};
 
struct _IFastStringVtbl
{
int (*AddRef) (IFastString *pIFastString);
int (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
};
 
// 增加接口指针的引用计数
#define IFASTSTRING_AddRef(p) ((IFastString*)p->pvt)->AddRef(p)
 
// 释放目标字符串对象
#define IFASTSTRING_Release(p) ((IFastString*)p->pvt)->Release(p)
 
// 获取目标字符串长度
#define IFASTSTRING_GetLength(p) ((IFastString*)p->pvt)->GetLength(p)
 
// 查找字符串,返回偏移量
#define IFASTSTRING_Find(p,s) ((IFastString*)p->pvt)->Find(p,s)
#endif // FASTSTRING_H_
       相应的实现文件如下:
FastString.c接口实现文件第五版
#include ”FastString.h”
#include <string.h>
 
typedef struct _CFastString {
    IFastStringVtbl *pvt; // 指向虚拟函数表的指针
int m_nRef;
char *m_pString;    // 指向字符串的指针
int m_nLen;        // 存储字符串的长度
} CFastString;
 
// 函数声明
static int IFastString_AddRef(IFastString *pIFastString);
static int IFastString_Release(IFastString *pIFastString);
static int IFastString_GetLength(IFastString *pIFastString)
static int IFastString_Find(IFastString *pIFastString, char *pFindStr);
 
IFastStringVtbl gvtFastString = { IFastString_AddRef,
                           IFastString_Release,
                           IFastString_GetLength,
                           IFastString_Find
                          };
 
// 创建目标字符串对象
void IFastString_CreateObject (IFastString **ppIFastString, unsigned int nUserData)
{
CFastString *pMe = malloc(sizeof(CFastString));
char *pStr = (char *)nUserData;
 
if(pMe == NULL pStr == NULL)
{
    return;
}
 
pMe->pvt = &gvtFastString;
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
 
pMe->m_nRef = 1;
* ppIFastString = (IFastString *)pMe;
 
}
 
// 增加接口指针的引用计数
static int IFastString_AddRef(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
 
return (++pMe->m_nRef);
}
 
// 释放目标字符串对象
static int IFastString_Release(IFastString *pIFastString)
{
    CFastString *pMe = (CFastString *)pIFastString;
   
if(–pMe->m_nRef >0)
{
    return pMe->m_nRef;
}
 
if(pMe->m_pString)
{
    free(pMe->m_pString);
}
 
free(pMe)
}
 
// 获取目标字符串长度
static int IFastString_GetLength(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
 
    return strlen(pMe->m_pString);
}
 
// 查找字符串,返回偏移量
static int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
CFastString *pMe = (CFastString *)pIFastString;
 
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
       在第五版的FastString中,我们主要是增加了接口AddRef。这个接口是在增加指针引用的时候调用的,它仅仅增加了接口内部变量m_nRef。同时为了实现相应的机制,在Release方法内增加了对引用计数的条件判断:如果当前的引用计数不为零,则直接返回引用计数的值,否则释放创建接口实例时所分配的内存。这样,当我们在使用上面的FastString_Test函数之前,调用IFASTSTRING_AddRef方法,就解决了资源管理的问题了。
       增加这个AddRef方法还有一定的人为因素,因为只要程序员能够足够的注意,那么就不会存在资源管理的问题。但是,人生不如意十有八九,我们不能将全部的希望都寄托在程序员的身上,谁都有犯错误的时候。因此增加了AddRef的约定,它与Release方法相对应,形成了一种对称的编程“美感”,约定了是要增加了对指针的引用就调用AddRef,对应的在在适当的位置使用Release释放指针引用。
       细心的读者可能还会发现第五版的FastString的实现中有两处不一样:一是IFastString_CreateObject函数的参数变化了,由原来的char *类型成了现在的unsigned int类型,这样做的目的是为了统一在Shell程序中CreateObject的形式;二是FastString接口函数中if(pMe == NULL)的判断去掉了,我们知道,在第一版的FastString中这句判断是很有必要的,它可以检测当前指针的有效性,那么想象一下对于我们的虚拟函数表NULL指针意味着什么?根据我们已有的虚拟函数表的知识,调用的接口是基于IFastString指针的相对偏移量,例如使用NULL指针来调用Release接口,那么实际上调用的是基于0地址的4字节偏移位置的函数,只有天知道这个地址中存储的是什么东西!因此,为接口传递空指针的时候会不可避免的发生异常,根本就不可能执行到接口函数,所以相应的判断是没有任何意义的。相应的示意图如下:
图5 接口的偏移量
       最后,再让我们清楚地看一下这种虚拟函数表所实现的总体框图吧,看看应用程序、接口定义以及接口实现之间的关系:
图6 应用程序、接口定义以及接口实现的关系
       至此,我们已经完成了全部需要解决的问题,最终我们实现的第五版FastString就是BREW接口的实现方法,只不过在实现的细节上有所不同。同时,我也相信各位在阅读了第二部分之后,一定会对这一部分的介绍颇有感触。接下来我们就趁热打铁,介绍BREW实现方法的一些高级特性,其中涉及了面向对象和COM组件的相关知识,对于没有接触过这两部分内容的读者来说,阅读可能会有一定困难。不过我会尽我最大的努力,争取用最容易理解的方式来阐述这些特性。
7.面向对象的特性
       C语言本身没有面向对象的特性,但是我们使用C语言开发的BREW就具有了面向对象的特性了。面向对象的主要特征是:数据抽象、继承和多态。数据抽象指的是使用一组数据和方法描述一个我们要表达的内容(对象),它的关键点在于将方法和数据结合也叫做封装;继承是指一个对象可以通过某种方式使用另一个对象中的方法和数据,它包含了语法上的继承和二进制层次的继承;多态是指通过虚拟函数实现的成员函数晚捆绑的特性,其核心的特征是成员函数的晚捆绑。
       BREW具有良好的数据抽象特性,它将全部的数据封装在了接口实现的内部,只将接口函数暴露给外部使用。这种方式是实现数据封装的理想方式,我想这一点可以从前面的“封装性”一节看出来。因此,BREW已经具有了面向对象的第一个特性。
       在这个最好的封装实现的基础上,根本没有任何数据直接暴露出来,因此,继承性就体现在接口的继承上面了。由于BREW是使用C语言实现的,因此没有办法实现语法上的直接继承,但是它实现了二进制层次上的继承。继承在二进制上的表现就是在本数据结构中兼容另一个数据结构。例如,假设结构体A中包含了成员int i,现在有结构体B包含了同样的int i类型的成员,并且后面紧跟了int j成员,此时B结构是A结构的超集,也可以说B结构继承了A结构。现在我们已经知道BREW接口的二进制结构,如果需要实现二进制的BREW继承,我们就可以通过定义一个接口A的超集来实现另一个接口B,此时我们就可以说这个接口B是从接口A继承来的。我们可以通过接口A的定义使用接口B中的方法。因此说,BREW具有面向对象的继承的特性,只不过不是在语法上,而是在二进制的数据结构层面。
       面向对象的第三个特性就是多态,可以说这个特性是BREW天生的。我们知道BREW一开始就是通过VTBL来实现的,可以实现运行时的接口函数绑定(也就是迟绑定)。关于这里点我们可以从五个版本的FastString的实现中发现这一过程。晚捆绑与早捆绑的区别在于早捆绑在使用接口的时候再编译链接的时候就已经确切的知道每个接口函数的地址,这就类似于使用一个C语言的库;而晚捆绑则是在运行时才知道每个接口函数的确切地址,因为虚拟函数表是在运行时才赋值给相应的接口的。使用这种技术的一个动机是我们可以在运行的时候控制使用一个接口中的多个实现中的哪一个实现。如果当前FastString接口已经公开了,那么可能会有第三方的厂家按照这个接口规范,来实现一个更好的FastString接口。与最原始的FastString接口布局一样,新的实现的接口函数布局与老的一样,那么此时仅仅需要更新一个Class ID我们就可以切换到新的接口了。使用晚捆绑的另一个动机是,应用程序可以检测到当前的运行平台是否已经实现了此接口,并给予相应的处理,这样可以避免在接口为实现的平台上运行时引起致命的错误。
       综上所述,BREW具有了面向对象的三个主要特性,因此说他是具有面向对象性质的一种开发平台。
8.与COM的比较
       COM是Windows平台上实现的一种跨语言的开发机制,目前在Windows平台的底层,许多功能都是通过COM机制来实现的。COM通过统一的、独立的接口定义语言(IDL:Interfase Definition Language)来定义统一的接口,并规定了相应的接口二进制规范,这样就可以按照这个二进制规范,通过各种不同的开发语言来实现COM程序的开发,而实现这种接口与实现之间完全分离的技术就是虚拟函数表(VTBL)。
       熟悉COM开发的读者对BREW应该有一种似曾相识的感觉,没错,在我的定义中,BREW就是一个简单版本的COM。BREW与COM相比,它们的核心思想是十分一致的,都具有接口与实现分离的特性,都使用了VTBL的技术等等。它们的不同点主要表现在以下几个方面:
       第一,它们的接口与实现间的分离程度不同。COM是接口和实现的完全分离,为此专门规范了统一的接口定义语言,因此而接口的实现可以采用任何一种开发语言,如C/C++和Java等。而BREW则为了简化开发,使用了C语言形式的接口定义,这样就使得BREW的应用程序和实现都需要基于同样规则的编译器。或者换句话说,COM实现是与开发语言无关的,而BREW的实现则是与语言相关的。
       第二,它们的接口创建方式不同。COM通过Windows注册表,使用文本的名字寻找相应的Class ID(这个Class ID需要通过注册程序进行注册),例如FastString接口可以通过传递字符串“FastString”最为参数从而创建FastString接口,实现这个功能的基础是运行时的类型识别技术(RTTI,相关的内容可以参考VC的书籍)。而BREW则为了简便起见,仅仅通过Class ID来创建接口。
       第三,它们的规范层次不同。COM是二进制层次的规范,只要符合COM的二件制规范,我们可以使用任何一种语言进行开发。而BREW则是一种开发语言上的规范(使用了统一的C语言接口定义)。当然,我们也可以把BREW做为一种二进制层次的规范,但是似乎这并不大适合于在嵌入式系统中应用程序,因为当前大部分嵌入式系统都是使用C语言来开发的。不过对于BREW应用程序来说,只要遵循BREW的调用约定,是可以使用其它语言开发的。这里就不详细的讨论这个问题了。
9.小结
       本章介绍了BREW实现的来龙去脉,展示了BREW所需要解决的两个主要问题:如何启动程序以及如何调用平台中的函数。最终,我们通过五个版本的FastString事例解决了全部的问题。BREW的本质是通过虚拟函数表技术实现了接口定义与接口实现之间的分离,这样BERW应用程序就可以存储在文件系统中,并在运行的时候调用接口函数。可以这样说,BREW已经将C语言运用到了极至,理解了BREW的原理可以对程序的开发有更加深入的认识。

 
 

Things you can do from here:

 
 
Reply all
Reply to author
Forward
0 new messages