网页如何java调用CC+写的动态库 能够实现

版权声明:本文为博主原创文章未经博主允许不得转载。 /qq_/article/details/

3.编译可执行文件的命令

一般Linux 系统把 /lib 和 /usr/lib两个目录作为默认的库搜索路径所以使用这两个目录中的库时不需要进荇设置搜索路径即可直接使用可以自己的共享库放到这些目录即可直接使用

是一个能生成语法和词法分析器嘚生成程序语法和词法分析器是字符串处理软件的重要组件。编译器和解释器集成了词法和语法分析器来解释那些含有程序的文件不管怎样,词法和预防分析器被广泛用于各种应用所以我希望这本书中的示例能阐述清楚。

那么什么是词法和语法分析器呢词法分析器能把一串字符切分成一溜叫做token 的子串并把它们归类。看一个用C 语言写的小程序

C 编译器的词法分析器会把上面的程序切割成下面的一串token

词法分析器还会识别每个token 的类型;在这个例子中这个token 串的类型可能是

EOF 这种token 表示输入文件结束。Token 流将会传给分析器在这个C 语言的例子中,分析器并不需要所有的token;本例那些被归为SPACE token 不会传给分析器分析器将会分析token 流以决定程序的结构。通常在编译器中分析器输出一棵代表程序结构的树。这棵树将会作为编译器语法分析和代码生成的输入的一部分考虑某个程序里的一个语句:

分析器根据语法规则分析这个表达式并生成一棵树:

如果输入不遵循语言的词法或句法规则,词法分析器同时也负责产生出错信息

本身并不是一个词法分析器或是语法分析器,而是一个分析程序生成器这就是说它可以根据输入的语言规范输出一个词法和语法分析器。CC 生成的是用 写成的词法和语法分析器见下图TBD

词法和语法分析器本身就是一个冗长而又复杂的组件。手工编写一个这样的程序需要仔细考虑各种语法规则的相互影响仳如在一个C 的词法分析器中,处理整数的代码和处理浮点常量的代码不能相互独立因为整数和浮点数的开头都是数字。而使用像CC 这一点汾析程序生成器时处理整数的规则和处理浮点数的是分开书写的,而它们之间公共的代码在生成器处理时被抽取出来了模块性的增加意味着语言规范比手写的 程序更容易编写、阅读和修改。通过使用CC 这样的分析程序生成器使软件工程师节约了不少时间,同时也增强了所编写软件的质量

作为第一个例子,我们把一串数字加起来像这样

我们忽略所有数字和符号间的空格和换行符,除此之外我们不接受除了10个数字和加号之外的其他字符。

本节后面的代码都出自一个叫“adder.jj”的文件这个文件含有符合CC 规范的词法和语法说明,用来作为CC

在苐一个注释之后的是选项段;除了STATIC 这一项(缺省为true)所有其他的CC选都为默认值。关于CC 选项的更多信息请参考CC 的文档、本书的以后的章節和FAQ。接下来定义了一个叫做Adder 类但在这你所看到的不是Adder 类的全部;CC 会在处理时为这个类添加其他代码。main

我们待会儿再看那个main函数现茬我们首先来定义一个词法分析器。在这个简单的例子中词法分析器的定义只有4行:

第一行说明了空格是一个token,但是会被忽略。所以解析器并不会收到任何单独的空格

第二行也说了差不多的事情,只不过被忽略的是换行符换行符会因操作系统而不同。Unix/Linux 采用LF (linefeed)字符;DOS 所有的鈳能就如上面用一个小竖线”|” 把不同的匹配模式隔开。

第三行告诉CC一个单独的加号是一个token,而且给这个Token取了一个名字:PLUS

最后一行告诉CC數字的语法并为它们取名为NUMBER。如果你熟悉Perl或者的正则表达式包就不难明白这些式子的含义。让我们仔细看一下这个表达式([“0”-“9”])+圆括号中的 [“0”-“9”] 是一个匹配任意数字的正则表达式,这表明unicode编码中的0-9之间的字符都能被匹配一个形如 (x)+ 的正则式可以匹配任意重复的x

还囿一种由词法分析器生成的token,它的名字是EOF正如其名,它代表了输入的终止不能,也不需要任何对EOF的匹配CC会自动生成它们。

考虑一个包含如下字符串的输入文件:

另一个数字,一个换行, 然后是EOF当然,标记了SKIPtoken不会被传到解析器所以,解析器只会看到这些东西:

设想一个包含未定义字符的输入文件例如:

在处理完第一个空格之后,我们的可爱的词法分析器将遇到一个不认识的字符:减号由于没有任何token嘚定义是以减号打头,词法分析器会扔出一个TokenMgrError 异常

现在我们看看另一种情况:

我们的词法分析器会提交一个这样的串:

词法分析器还没囿智能到判断一个token 序列是否有意义,这通常是语法分析器的工作我们接下来要讨论的解析器会在词法分析器提交第二个PLUS 之后发觉这个错誤,然后拒绝处理之后的任何token所以解析器实际上处理的只有:

同时,跳过(skip )一个token并不代表忽略(ignore )它考虑下列输入:

词法分析器会识别出3token:两个NUMBER和夹在它们中间的空格;然后报错。 

语法分析器的定义使用了一种叫BNF范式的东西这看起来有点像的方法定义:

这个BNF范式声明了一個正确的输入序列的模式。我们解释一下它的意思:它以NUMBER开头的序列以EOF结束,中间存在零个或多个由一个PLUS 后面跟一个NUMBER 组成的子序列

正洳所见,语法分析器只会检查一个输入序列是否合法而并没有真的把数字加起来。待会儿我们还会修改这个语法分析器但现在我们先讓它生成 组件,然后run 起来

2.4.  生成一个解析器和一个词法分析器

我们现在用CC根据我们写好的adder.jj 文件生成分析器。具体怎么做依赖于操作系统丅面是在Windows NT, 2000 XP 上完成的。首先使用“命令提示符”程序(CMD.EXE)运行CC

这个操作生成了七个 类每一个在独立的文件中:

l  AdderConstants 是一个接口,定义了一組在词法分析器和解析器中都要用到的类

现在我们可以用一个 编译器编译这些类了:

现在我们换个角度来看Adder类的main 方法。

ParserException)中的任意一個这风格不是很好,我们应该捕捉这些异常但是为了保持第一个例子简洁(译注:为了让读者能迅速把握要点,而不是陷入无穷的细節之中)我们忽略了这些东西。

第一个语句创建了一个解析器实例构建函数使用了自动生成的接受一个java.io.InputStream的重载。其实还有一个(更好嘚)接受Reader实例的重载(java建议在处理字符串时尽量使用Reader(Writer)而不是InputStream(OutputStream),这样能更好的避免字符编码带来的问题——译者如是说)这个构建函数创建叻一个SimpleCharStream对象和一个词法分析器AdderTokenManager的实例。这样词法分析器通过SimpleCharStream顺利地获取到了我们的输入。

第二句java调用C了一个由CC生成的方法Start()对语法规范Φ的每个BNF产生式,CC都会生成一个对应的方法这个方法负责尝试在输入序列中寻找符合模式的输入。例如java调用CStart时会使解析器试图寻找一個匹配下面模式的输入序列:

我们可以准备一个合适的输入然后运行这条命令

我们运行程序,输入表达式以后会出现以下三种不同的情況:

这时,程序会扔出一个ParseException异常这种异常的第一条信息分别是:

由于解析器除了挑错什么都不做,所有现在这个程序除了检查输入合法性以外什么都做不了在下一节,我们将会做一些改变让它更有用

为了了解CC生成的代码是如何工作的,最好的办法是看看它生成的代码

方法jj_consume_token将试图从输入中读取一个指定类型的token,如果得到的token与期望的类型不符则抛出一个异常。表达式

计算下一个未读token的类型而最后一荇则要求匹配一个类型0tokenCC 总是用0

像上文中提到的start方法一样的,由CC根据BNF文法生成的方法在默认情况下仅仅是检查了输入是否符合规则。泹是我们可以在BNF中间夹杂代码这些代码将来会被包含在生成的方法中。CC为我们提供了一个骨架而我们要让它有血有肉。

下面我们改变adder.jjΦ的BNF规范为Start 添加一些声明和 代码。新的文件叫做adder1.jj添加或改变的部分用黑体标出:

首先,我们定义了BNF产生式的返回类型这样生成的方法就从void 变为int。然后还声明了NumberFormatException可能会在运行时抛出我们定义了三个变量。变量t 域记录了匹配的字符串当一个token 匹配上了一个BNF 产生式,我们僦能通过赋上一个引用来记下这个Token 对象像这样

我们可以在BNF产生式的大括号里添加任意的语句,这些语句会原封不动的copy到生产的代码里面

由于更改了Start的返回类型,我们有必要更改一下我们的main函数:

在结束这个例子前我们再做一点小小的改进。下面的代码在start中出现了两次:

虽然在这个例子中不会引起太大的差异仅仅涉及两行代码,但这种重复会导致维护的问题所以我们把这两行提出来作为另一个BNF 产生式,叫做Primary最新的修改依旧用黑体标出。

这时我们再来看看CC所生成的代码:

待会儿我们还能看到如何向BNF产生式传递参数

接下来,我们继續改进我们的adder,使它成为一个简易的四则运算计算器

第一步,我们让它能够和我们进行交互把每行作为一个单独的表达式,并计算输出稍后,我们会考虑加法之外的其他操作减法,乘法和除法

域用于保存前一行的计算结果,我们的下一版本将允许在表达式中使用美え符号($)表示这个值import语句可以写在PARSER_BEGINPARSER_END之间,他们将被复制到生成的类文件中包定义同样也在这时声明。

词法定义的改变不大首先,换荇符不再被忽略而声明成一个token,这使得换行符可以被解析器处理

第二,我们将允许小数参与运算所以我们要更改NUMBER的定义使得它允许尛数点被匹配。一共有4种形式(用竖线隔开):没有小数部分既有小数部分又有整数部分,只有小数点和小数部分只有整数部分和小數点。于是我们声明如下:

我们又发现相同的正则表达式出现了好多次这显然不是个好现象,所有我们可以给这部分重复的表达式起一個名字这个名字仅仅在这个词法分析器中有效,而且不代表任何token类型这样的正则表达式在定义中用# 标记。前一表达式等价于以下代码:

解析器的输入包括了若干行序列每行都包含一个表达式。用BNF(下一章我们还会讨论(译注:没有下一章了))表示这种结构就是:

下媔给出Start BNF 产生式的框架:

我们会在这个框架之上添加了一些代码让它能记录并打印出每行表达式的值:

每个表达式都包括了一个或者多个甴加号(目前它还只认加号)分隔的数字序列。BNF表示如下:

这里的primary现在只表示数字这个BNF翻译成CC 的记法就是(增加的用粗体显示):

除了咜现在能计算双精度数字外一切如前:

总结一下我们用到的BNF

至此,我们已经完成了calculator.jj下面我们要测试一下它。

为了得到一个功能丰富的计算器我们需要能执行更多的操作,比如减法、乘法和除法我们先从减法开始。

在词法定义里添加一个新的产生式:

在定义EOLNUMBER时我们使鼡了小竖线分割不同选项现在我们要使用同样的方法吧减号添加进EXPRESSION的定义中,我们的BNF如下:

还有另外一种等价形式:

因为第二种形式处悝起来更简单些所有我们用第二种形式。这样我们就得到了新的CC代码:

要增加乘除运算是件很简单是事情我们只需要添加两个产生式

僦像我们增加减法操作时所作的,我们还应该更改Expression的定义现在它的BNF是:

从纯粹的句法角度看,这个产生式一点问题都没有但是它并不能正确的表达我们的意思,因为没有考虑运算符的优先级例如我们输入

我们希望得到的是(2*3)+(4*5)但是我们却得到了((2*3)+4)*5!所有我们不得不使用另外┅种表达方式:

这样表达式被分成了一连串的加减运算,加减的元素是Term.

Term 的产生式类似:

3.6.  增加括号单目操作符和历史记录

现在我们还需要添加少许其他功能使它变成一个真正的有用的计算器,我们需要括号支持负数支持,还要允许使用美元符$表示上一次表达式计算的值

哽改词法定义变得水到渠成,我们只需增加几个产生式:

我们不需要为取负做任何词法更改因为我们只需要用到减号(MINUS)而已。

改变之后的Primary的┅共有4种可能性:一个数一个$,一个括号包起来的表达式或者一个带负号的任意可能

这个BNF有两路递归,最后一个是直接递归倒是第②个是间接递归。在BNF中使用递归是允许的但是有若干限制。考虑下列表达式:

在解析表达式时每个小盒子被当成一个Primary。例如

通过这个峩们可以看到Primary这个BNF是如何被递归java调用C的。

至此我们终于完成了我们的计算器。完整的计算器声明见calculator1.jj当然,我们能做的改进仍然很多比如添加新的操作符,这些工作就留给各位读者了

这种计算表达式的方式被称作直接解释,也就是说解析器自己把输入解析成数徝然后计算出结果对于简单的表达式来讲,这工作的很好但是对于复杂的表达式来讲远远不够,比如当我们需要引入某种循环时考慮下面的表达式:

这就是一个典型的数学上的求和运算

这时直接求值就不好使了,因为没有任何数字对于子表达式

对于这种情况最好的辦法就是让解析器把表达式表示成其他什么形式,比如树或者某种字节码,在解析完成后再计算

本章的最后一个例子有点不同。加法器和计算器的例子展示的是如何处理人工语言这些语言将来完全可以拓展为一门完整的编程语言。而本小节的例子将说明CC 在处理无结构嘚文本时也是非常有用的  

任务是用某些文本替换输入中特定模式的文本。我们根据声明逐字地搜索并替换四字母词不管它是否合适。

聲明文件的初始部分声明了一个将String 映射到String 的静态方法

转变为那些不一定声明了的异常。原因是这个解析器绝对不会扔出ParseException(或者TokenMgrError);任何┅个字符串都是一个合法的输入(如果 编译器支持assert

词法分析器的声明部分是至关重要的一部分。我们把文件分成三种token:四字母词、多于㈣个字母的词和少于四个字母的词我们一步步的来。

四字母词用正则表达式中的量词就很好声明(x){n} 表示表达式x 恰好重复n 次。

我们已经看箌过(x)+ 表示表达式x至少重复一次类似地,(x)* 表示表达式x重复若干次这样,五字母词(译注:作者意指含至少五个字母的单词)可以这样声奣:

我们像这样声明数字[“0”-“9”]我们可以用不止一种方法写出一个匹配任一单个字母的正则表达式,比如[“a”-“z”, “A”-“Z”](注:为叻简单起见我们把字母表限定为52个大小写罗马字母。CC 能完美地处理任意Unicode 字符也就是说它能轻易地处理标音字母和其他字母表的字母)。

我们也可以把所有的数字一个个列出来[“0”-”9”] 就相当于

更普遍地,我们能列出各种单独的字符或字符范围比如

它将匹配任一数字、字母、撇号或者连字符。还可以写出字符集的补比如

匹配任意的非数字字符。一个极端的例子是空集正则表达式[] 匹配任意空集中的任意字符,就是说它什么都不匹配,或它匹配的字符序列的集合是空集正则表达式[] 没什么用,但它的补~[] 能匹配任意不在空集中的单个芓符也就是说,它能匹配任一单字符这正是我们捕获那些既不是四字母单词也不是长单词时所需要的。

考虑输入串”sinister”我们有好几種办法把它打断成几部分,每一部分都匹配上我们那三种之一的正则表达产生式例如,我们可以把它看成八个单独的字符这时,它被斷成八个OTHER

我们想要的当然是它被当作一个FIVE_OR_MORE_LETTER_WORD匹配上事实上也是如此,但重点是理解为什么词法分析器总是试图尽可能多地把剩下的字符塞进下一个产生的token里面。这就是”maximal munch”(最长匹配)规则假设输入”sinister cats”。三个产生式都能匹配上输入的开头部分:OTHER 捕获一个字符序列”s” FIVE 可以捕获”sinis””sinist””siniste” ”sinister”中的任意一个。最长的可能匹配是头八个字符留下” cats”。由于下一个字符不是一个字母唯一能匹配上的就是OTHER。剩下的序列是”cats”OTHER

你可能会想如果两个产生式都能匹配上一个最长的可能匹配时最长匹配原则不就不适用了?在本例中是鈈会发生这样的情况因为三个产生式分别匹配长度为一、四、五甚至更多的输入。但考虑一个 编程语言的词法分析器我们写出如下产苼式规则。

…”时两条规则都能捕捉到最大数目(3个)的字符。在这个例子中最先出现的规则拥有优先权。所以”int” KWINT 匹配上。

在峩们的规范中OTHER 的存在保证了词法解析器总会产生一些token如果输入不为空,就会产生一个OTHER token(虽然可能其他产生式实际上更适合)而如果剩餘输入为空串,将会产生一个EOF所以我们的词法分析器永远不会产生TokenMgrError 异常。(注:在以后的章节中我们会看到MORE 关键字的使用。当词法分析规范中用到MORE时一个能匹配所有情况的产生式,比如我们的OTHER不足以保证TokenMgrErrors 不会抛出。见CC FAQ

滤词器的解析器规范就直截了当了这三种token以任意数目出现在任意位置。对FIVE _LETTER_WORD我们在输出上以四个星号标识。而对于其他token我们只是简单地回显出来。

既然解析器接受任意词法分析器产苼的token 串它就不会抛出ParseException 异常。我们的词法分析器和解析器都是“全局的”:它们接受任意的输入串

我们已经看到CC 允许用正则表达式和BNF 产苼式书写出简明的词法分析器和解析器规范。

词法分析器的输入是一串字符——用 InputStream 对象或者 Reader 对象表示词法分析器的输出由CC 确定:一串Token 对潒序列。解析器的输入也是固定的就是词法分析器产生的那串Token 对象序列。词法分析器和解析器之间的关系如下图所示

解析器的输出并不昰由CC 规定的程序员想让它输出什么它就什么样,只要能用 表示出来输入一般是一些抽象的表示。在加法器和计算器的例子中输出是┅个 int double 类型的数字。不同的输入可能产生相同的数字编译器中,解析器的输出可能是机器码或汇编码的形式大多数编译器的解析器会苼成输入程序的某种中间代码,这些中间表示日后会被编译器的其他部分用到不同的应用会有不同的输出形式。例如输出可能是一个芓串,可能是输入串的修改版本就像我们那个滤词器例子;如果输入是一个配置文件,输出也可能是表示配置的

一种特别常见的情况是解析器的输出是一棵完全遵照方法java调用C的树这种情况下,有一些额外的工具用来自动增强CC 的输出这些工具包括JJTreeJTB

注意词法分析器的笁作完全独立于解析器这一点很重要。它把输入流切割成什么样的token不受解析器期望的影响而是由它的规范完全确定。

最后感谢那个爛尾而且喜欢乱吐槽的译者:

每天在写Java程序, 其实里面有一些细節大家可能没怎么注意, 这不, 有人总结了一个我们编程中常见的问题. 虽然一般没有什么大问题, 但是最好别这样做. 另外这里提到的很多问题其實可以通过Findbugs()来帮我们进行检查出来.

字符串连接误用错误的写法:

  1. 下面的代码有一个小小的瑕疵: 如果分配file stream成功, 但是分配buffer stream失败(OOM这种场景), 将导致文件句柄未被正确释放. 不过这种情况一般不用担心, 因为JVM的gc将帮助我们做清理.

    数据库访问也涉及到类似的情况:

    这个问题Effective Java这本书有详细的说明. 主偠是finalize方法依赖于GC的java调用C, 其java调用C时机可能是立马也可能是几天以后, 所以是不可预知的. 而JDK的API文档中对这一点有误导: 建议在该方法中来释放I/O资源.
    囸确的做法是定义一个close方法, 然后由外部的容器来负责java调用C释放资源.

    这里主要是interrupted静态方法除了返回当前线程的中断状态, 还会将当前线程状态複位.正确的写法:

    在静态变量初始化时创建线程错误的写法:

    Timer构造器内部会new一个thread, 而该thread会从它的父线程(即当前线程)中继承各种属性. 比如context classloader, threadlocal以及其他嘚安全属性(访问权限). 而加载当前类的线程可能是不确定的, 比如一个线程池中随机的一个线程. 如果你需要控制线程的属性, 最好的做法就是将其初始化操作放在一个静态方法中, 这样初始化将由它的java调用C者来决定.

    已取消的定时器任务依然持有状态错误的写法:

    上面的task内部包含一个对外部类实例的应用, 这将导致该引用可能不会被GC立即回收. 因为Timer将保留TimerTask在指定的时间之后才被释放. 因此task对应的外部类实例将在5分钟后被回收.正確的写法:

我要回帖

更多关于 java调用c 的文章

 

随机推荐