第二十五章 可移植的 Perl
如果世界上只有一种操作系统,那么移植就会非常容易,但生活就会单调乏味。我们更喜欢有
一大堆操作系统物种,只要我们的生态系统不要过于清晰地分成捕食动物和被捕食动物。Perl
可以在十几种操作系统上运行,并且因为 Perl 程序是平台无关的,同一个程序可以运行在
所有这些系统上而不需要修改。
不错,是几乎不用修改。Perl 试图给程序员尽可能多的特性,但是如果你使用了属于某一操作
系统的特殊特性,那么你就可能减少了你的程序向其他操作系统移植的机会。在本章里,我们
将提供一些书写可移植的 Perl 代码的原则。一旦你决定了可移植性的程度,那么你就能明白
界限在哪个位置,并且你就可以呆在里面。
从另外一个方面来看,书写可移植的代码通常就是有意限制你可以使用的选择。自然,这么干
是需要纪律和牺牲的,这两个要求可能是 Perl 程序员不太习惯的东西。
请记住:不是所有 Perl 程序都需要移植。我们没有理由不用 Perl 把 Unix 工具粘合在
一起,或者写一个 Macintosh 应用的原形,或者用来管理 Windows 的注册表。如果牺牲
移植性是值得的,那么做牺牲呗。(注:不是任何对话都必须在多文化环境里正确。Perl 试图
给你至少一个方法正确地处理事物,但是它并没有强制你如何做。在这方面,Perl 更象你的
母语而不是保姆的语言。)
通常,我们要记住用户 ID,“家”目录,以及登陆状态等概念只有在多用户平台上才有。
特殊的 $^O 变量告诉你你的 Perl 是制作在什么操作系统上的。这个变量提高了程序的
速度,否则程序就需要使用 use Config 通过 $Config{osname} 获取相同的信息。(就
算你因为其他原因使用了 Config,这个变量也因为不用进行高负荷的散列查询而节约你的
时间。)
要获取关于平台的更多信息,你可以在 %Config 散列中寻找剩下的信息,你可以
在标准的 Config 模块中找到它。比如,要检查平台是否有 lstat 调用,你可以检查
$Config{d_lstat}。参阅 Config 的在线文档获取可用变量的全部描述,以及参阅
perlport 手册页获取 Perl 内建的函数在不同平台上的行为列表。下面是随着平台的变化
其行为也变化的 Perl 函数:-X(文件测试)。。。(略)
25.1. 换行
在大多数操作系统里,文件里的行是用一个或者两个标志着行结尾的字符结束的。这些字符
因不同系统而异。Unix 通常使用 \012(也就是在 ASCII 字符里的八进制数为 12 的
字符),DOS 类的 I/O 使用 \015\012,而 Mac 系统使用 \015。Perl 使用 \n 代表
“逻辑”上的新行,而不管平台是什么。在 MacPerl? 里,\n 的意思总是 \015,而在
DOS 类的 Perl 里,\n 通常表示 \012,但是如果以“文本”模式访问一个文件,那么它
就根据你是读还是写相应翻译成 \015\012 或 \012。在规范模式里,Unix 里在终端上也
做同样的事情。\015\012通常被称为 CRLF。
因为 DOS 在文本文件和二进制文件之间是有区别的,所以 DOS 类的 Perl 在“文本模式”
里使用 seek 和 tell 的时候会有限制。为了获得最好的结果,我们应该只 seek 那些 tell
返回的位置。不过,如果你在该文件句柄上使用 Perl 内置的 binmode 功能,那么你还是
可以不加思索地使用 seek 和 tell。
套接字编程的一个常见的错误概念是在任何地方 \n 都是 \012。在许多常见互联网
协议里,\012 和 \015 都是指定的,而 Perl 的 \n 和 \r 的数值是不可靠的,因为
它们因系统的不同而不同:
print SOCKET "Hi there, client!\015\012"; # 正确 print SOCKET "Hi there, client!\r\n"; # 错误
不过,使用 \015\012(或者 \cM\cJ,或者 \x0D\x0A,甚至是 v13.10)都会是非常乏味
并且难看的,而且还容易让那些维护代码的人员觉得糊涂。Socket 模块为那些有需要的人
提供了一些好东西:
use Socket qw( :DEFAULT :crlf); print SOCKET "Hi there, client!$CRLF" # 正确
当从一个套接字读取数据的时候,请记住缺省的输入记录分隔符 $/ 是 \n,这就意味着
如果你不知道通过网络之后看到的会是什么东西,那么你就需要做一些额外的工作。坚固的
套接字代码应该能识别行尾的 \012 或者\015\012:
use Socket qw( :DEFAULT :crlf); local ($/) = LF; #如果 $/ 已经是 \012 了就不需要这行 while () { s/$CR?$LF/\n/; # 用逻辑换行符代替 LF 或者 CRLF。 }
类似地,返回文本数据的代码——比如抓取 web 页面的子过程——通常应该翻译换行。
通常一行程序就足够了:
$data =~ s/\015?\012/\n/g; return $data;
25.2. 权重(字节序)和数字宽度
计算机以不同的顺序(高权重在前或者高权重在后)和不同的宽度(目前 32 位和 64 位的
是最常见的)。通常,你用不着考虑这些事情。但是如果你的程序通过一个网络连接发送
二进制数据,或者写到一个会在不同的计算机上阅读的文件里,那么你就得采取一些预防
措施。
顺序的冲突可以让数字完全错乱。如果一个高权重在前的主机(比如 Intel CPU)
存储 0x12345678(十进制305,419,896),那么一个高权重在后的主机(比如 Motorola
CPU)就会把它读成 0x78563412(十进制2,018,915,346)。为了避免在网络连接中出现
这种问题,使用 pack 和 unpack 的格式 n 和 N,它不管平台是什么都以高权重在后的
字节序写无符号的短整数和长整数(通常称做“网络”字节序)。
你可以通过打开一个本机格式的数据结构看看你的平台的字节序是怎样的:
print unpack("h*", pack("s2", 1, 2)), "\n"; # '10002000' 在 Intel x86 或者 Alpha 21064 在高权重在前模式下 # '0010000200' 在 Motorola 68040
要判断你的字节序,你可以使用这些语句之一:
$is_big_endian = unpack("h*", pack("s", 1)) =~ /01/; $is_little_endian = unpack("h*", pack("s", 1)) =~ /^1/;
就算两个系统有相同的字节序,那么在 32 位平台和 64 位平台之间传输数据仍然将有
问题。除了避免传输或者存储裸二进制数字以外没有什么很好的解决方法。要么用文本模式
传输和存储数字,或者使用象 Data::Dumper 或者 Storable 这样的模块替你干这个
事情。不过在任何情况下你都会非常希望使用基于文本的协议——它们更加强壮,更加容易
维护,并且比二进制协议更加具有扩展性。
当然,有了强大的 XML 和 Unicode,我们的文本定义变得更加灵活了。比如,在两个运行
Perl 5.6.0(或者更新的版本)的系统上,你可以把一系列整数编码成为 utf-8 (Perl
版本的 UTF-8)传递。如果两端都在一个有 64 位整数的机器上运行,那么你就可以交换 64
位整数。否则,你就局限于 32 位整数。使用带 U* 模板的 pack 发送数据,而使用带有
U* 模板的 unpack 接收。
25.3. 文件和文件系统
在 Unix 里,文件路径的部件是用 / 间隔的,而在 Windows 里是 \,而在 Mac 里是 :。
有些系统既不支持硬连接(link)也不支持符号连接(symlink,readlink,lstat)。
有些系统注意文件名字的大小写,有些系统不,并且有些系统只注意创建文件的时间而不在
乎读取它们的时间。
有一些模块可以协助你做这些事情。标准的 File::Spec 模块提供一些帮你做正确事情的
函数:
use File::Spec::Functions; chdir( updir() ); # 进入上层目录 $file = catfile( curdir(), 'temp', 'file.txt' );
最后一行在 Unix 和 Windows 上读成 ./temp/file.txt,在 Mac 里读成
:temp:file.txt,或者在 VMS里是 [.temp]file.txt,并且把文件内容存储在 $file
里。
File::Basename 模块,是和 Perl 捆绑的另外一个平台无关的模块,它把路径名分解成
各个组件:文件名,到目录的全路径,以及文件后缀。
下面是书写可移植的操作文件的 Perl 程序的一些建议:
- 不要使用同名的大小写不同的两个文件名,比如 test.pl 和 Test.pl,因为有些平台忽略大小写。
- 可能的情况下把文件名约束在 8.3 的习惯里(八个字母的名字和三个字母的扩展名)。如果你能通过墙上 8.3 尺寸的孔,那么只要你仍然保证文件名的唯一,通常你都可以不使用长文件名。
- 在文件名里把非字母数字减少到最少。使用下划线通常没有问题,但是它浪费了一个可能可以用于在 8.3 系统里唯一标识该文件的字母。(请记住,这就是为什么我们通常不在模块名字里放下划线。)
- 类似,使用 AutoSplit? 模块的时候,尝试把你的子过程名字限制在八个字符或者更少,并且不要用同名的大小写不同的名字命名子过程。如果你需要更长的子过程名字,将每个子过程名字 的前八个字母做成唯一。
- 总是明确地使用 < 打开一个文件用于读取;否则,在那些允许文件名有标点的系统上,一个前缀了 > 字符的文件可能导致把一个文件删除,而一个前缀 | 的文件名可能导致文件的管道打开。这是因为两个参数形式的 open 是智能的,并且会解释象 >,<,和 | 这样的字符,而这么做却可能是错误的。(当然也有正确的时候。)
open(FILE, $existing_fiel) or die $!; #错误 open(FILE, "<$existing_file") or die $!; #正确 open(FILE, "<", $existing_file") or die $!; #更正确
- 不要认为文本文件会以一个新行结尾。它们应该如此,但是有时候人们会忘记,尤其是在他们的文本帮助下。
- 系统交互
那些依赖图形用户界面的平台通常缺乏命令行工具,因此需要命令行接口的程序可能无法在任何地方都能够运行。对此你除了升级以外干不了什么事情。
一些其他的技巧:
- 有些平台无法删除或者重新命名正在使用的文件的文件名,所以要记住在你完成对文件的操作之后 close 文件。不要 unlink 或者 rename 一个打开了的文件。不要 tie 或者 open 一个已经捆绑或者打开了的文件;先 untie 或者 close 它们。
- 写文件的时候不要把同一个文件打开多于一次,因为有些操作系统在这样的文件上加了命令性的锁。
- 不要依赖在 %ENV 里是否存在特定的环境变量,并且不要认为任何在 %ENV 里的东西都是大小写敏感的或者是保留大小写的。不要认为 Unix 从环境变量中继承了语意;在一些系统上,它们可能是对其他所有进程都是可见的。
- 不要使用信号或者 %SIG。
- 尽量避免文件名聚集,而是使用 opendir,readdir,和 closedir。(到了 Perl 5.6.0,基本文件名聚集已经比原来的可移植性好多了,但是如果你想这么用,就会有些系统仍然和 Unix 的缺省接口冲突。)
- 不要认为错误数字或者字串的特定值存放在 $!。
- 进程间通讯(IPC)
为了使可移植性最大化,不要试图启动一个新进程。这就意味着你要避免 system,exec,fork,pipe,` `,qx//,或者带 | 的 open。
主要问题不是操作符本身的;通常,大多数平台都支持在进程外运行命令(尽管有些平台
并不支持任何类型的进程派生)。如果你调用的外部程序有名字,路径,输出或者参数语意
这些的在平台间不同的东西,那么很有可能就会发生问题。
一段特别流行的 Perl 代码就是打开一个到 sendmail 的管道,这样你的程序就可以发送邮件:
open(MAIL, '|/usr/lib/sendmail -t') or die "cannot fork sendmail: $!";
这段代码在没有 sendmail 的平台上可不能运行。一个可移植的解决方法是,使用 CPAN 的
一个模块来发送你的邮件,比如在 MailTools? 发布里的 Mail::Mailer 和 Mail::Send,
或者 Mail::Sendmail。Unix SystemV? IPC 函数(msg*(),sem*(),shm*())并不总是
可用的,甚至在一些 Unix 平台上也这样。
25.6. 外部子过程(XS)
通常 XS 代码可以制作成可以在任何平台上运行,但是库和头文件可能不是那么容易使用,
或者 XS 代码本身可能是平台相关的。如果库和头文件是可以移植的,那么我们就有理由相信
XS 代码也可以做得可以移植。
在写 XS 代码的时候,我们要面对另外一种类型的移植性问题:在终用户的平台上是否有 C
编译器。C 本身带来自己的移植性问题,而书写 XS 代码时,你会需要面对这样的问题。写
纯 Perl 代码是一个更容易实现移植的方法,因为 Perl 的配置过程绕开了那些非常烦人的
问题,为你隐藏了 C 的可移植性的问题。(注:有些在社会边缘生活的人甚至把运行 Perl
的 Configure 脚本当作一种廉价的消遣方式。有些人设立了称之为“Configure 竞赛”的
比赛,在不同的系统之间比较并且还投下巨注。现在许多文明的世界都宣布这样的比赛是不
合法的。)
25.7 标准模块
通常,标准模块(和 Perl 捆绑在一起的模块)可以在所有平台上运行。需要注意的例外是
CPAN.pm 模块(它现在需要与外部程序连接,而那些程序可能不存在),平台相关模块
(比如 ExtUtils?::MM_VMS),和 DBM 模块。
不是在所有平台上都有一个 DBM 模块可以用。SDBM_File 和其他的东西通常在所有 Unix 和
DOS 类的移植里面可以找到,但是在 MacPerl? 里没有,这个时候只有 NBDM_File 和
DB_File 可以用。
好的方面是至少能找到一个 DBM 模块,并且 AnyDBM?_File 会使用它找得到的任何模块。因为
有这样的不确定性,你应该只使用那些所有 DBM 实现都共有的特性。比如,令你的记录小于
1K 字节。参阅 AnyDBM?_File 模块的文档获取更多信息。
25.8. 日期和时间
如果允许,请使用 ISO-8601 标准(“YYYY-MM-DD”)表示时间。象“1987-12-18”这样的
字串可以很容易地用 Date::Parse 这样的模块转换成系统相关的值。一个时间和日期值的
列表(象那些内建的localtime 函数返回的值)可以用 Time::Local 模块转换成系统相关的
形式。
内建的 time 函数会返回自“纪元”(epoch)开始以来的秒数,但是不同的系统对纪元的
开始时间有不同看法。在许多系统上,纪元始于1970年一月一日,00:00:00 UTC,但是在
Mac 上这个时间早了 66 年,而在 VMS 上,它开始于 1858年十一月十七号,00:00:00。
因此,如果想使用可以移植的时间,你可能要计算一个与纪元的偏移量:
require Time::Local; $offset = Time::Local::timegm(0, 0, 0, 1, 0, 70);
在 Unix 和 Windows 里的 $offset 总会是 0,但是在 Mac 和 VMS 里,它就可能是一些
比较大的数字。然后就可以把 $offset 加到一个 Unix 时间上,获取任意系统上的同样的
时间值。
系统的当日时间和日历日期的表现形式可以用非常多的方法来控制。不要假定时区的数值存储
在 $ENV{TZ} 里。就算它存储在这个环境变量里,也不要认为你可以通过该变量控制时区。
25.9 国际化
在你的程序里使用 Unicode。在你的接口里做你和外界的字符集的来回的转换。参阅第十五章,
Unicode。
在 Unicode 之外的世界里,你可以假定字符集的一些特征但是你不能假定任何字符的 ord
值。不要认为字母字符的 ord 数值是连续的。小写字符可能在大写字符前面,也可能在它们
后面;小写字符和大写字符可能是交错在一起的,因此 a 和 A 都在 b 前面;重音符和其他
国际字符也可能相互交错,因此 a(重音符)在 b 前面。
如果你的程序准备在一个 POSIX 系统上运行(一个非常大的假定),参阅 perllocale
手册页获取更多关于 POSIX 区域的假设。区域影响字符集和编码,以及日期和时间格式,
还影响许多其他的东西。正确地使用区域可以让你的程序更加可移植一些,或者至少更方便
使用或者对那些非英文用户更友善。但是要注意区域设置和 Unicode 混合得不是那么好。
25.10. 风格
如果有必要使用平台相关的代码,那么请考虑把它们放在一个容易移植到其他平台的地方。
使用 Config 模块和在不同平台上声明变量 $^O 为不同的数值。
请仔细一些处理那些随着你的模块或者程序一起提供的测试程序。一个模块的代码可能是完全
可移植的,但是它的测试程序可能不是完全可移植的。这种情况通常发生在测试派生了其他
进程或者调用了外部程序帮助测试的情况下,或者是在(上面也提到了)测试对文件系统和
路径做了某些假定的情况下。请注意不要倚赖特定的输出风格来查找错误,甚至是在系统调用
后检查 $! 获取“标准”错误的时候也不要倚赖特定输出风格。应该使用 Errno 模块。
请记住好的风格是超越时间和国界的,所以,为了获取最大的可移植性,你必须寻找因你的
存在而衍生出的各种苛刻要求中间的共性。最酷的人不是那些最新时尚的囚徒;他们根本不配,
因为他们不用担心如何“存在于”他们自己的文化之中,不管是写程序还是别的什么东西。
时尚是变量,但风格是常量。
<!--
- Set MYTITLE = 第二十五章,可移植的 Perl
-->