Discuz! Board

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 1656|回复: 0
打印 上一主题 下一主题

如何写好一个跨平台的程序?

[复制链接]

1272

主题

2067

帖子

7958

积分

认证用户组

Rank: 5Rank: 5

积分
7958
跳转到指定楼层
楼主
发表于 2022-4-25 23:47:22 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
写跨平台C语言代码的十个规则2014年06月18日 编程 评论 1 条 阅读 5,121 次
摘要:

翻译了一篇关于写跨平台C语言代码的文章,介绍了10个相关规则。Rule #1: 同时开发 – 不要先开发后移植。不要把迁移工作外包。Rule #3: 使用标准C类型,不要使用特定于平台的类型。Rule #4: 只使用内置的 #ifdef 编译标志,不要自己发明轮子。 Rule #5: 开发一个简单的可重用的跨平台的基础库,来隐藏每个平台的代码。Rule 6#: 在所有的API中都使用Unicode(特别是UTF-8)。Rule #7: 不要使用第三方应用程序框架或者运行时环境来是你的代码跨平台。Rule #8: 原生代码本来就总是可以所有平台上编译->不是某个脚本让它们从不能编译变得能够编译了。 Rule #9:所有的程序都能在所有的平台编译。Rule #10:开除那些懒惰、不称职、态度差、不遵循这些规则的程序员。

Ten Rules for Writing Cross Platform 'C' Code

写跨平台C语言代码的十个规则


引言:

20多年来,我已经写很多了成功的跨平台C/C++代码。最近,在Backblaze公司,我们又开发了一款在线备份产品。这是一个运行在Windows或者Macintosh的小的桌面组件,它能加密用户文件并把文件通过互联网发送到我们位于San Francisco, California的数据中心。我们在Windows、Mac和Linux上使用了相同的可交换的C/C++库。我估算了一下,支持这三个平台大约多花了5%的时间(译注:应指相比只支持某一个平台)。但我也碰到过一些开发者、经理等,他们认为跨平台开发很难,或者花费时间会加倍三倍等。这个错误认识可能是因为他们曾经有过糟糕的代码移植经历。所以本文快速概述了在高效跨平台开发过程中,我所依赖的十个简单规则。


目标平台:Microsoft Windows/Apple Macintosh/Linux

我所说的这些概念适用于所有的平台,但是最主流的当然还是Windows,Mac,Linux。在Backblaze,我们把系统中用户安装的桌面组件部署到运行Windows和Mac的用户机器上以及我们数据中心运行的Linux上。在三个平台上,我们遵循了如下10条规则,使用了相同的C/C++代码。

我特别想澄清的是,我总是相信我们应该使用每个平台上各自最流行的编译器和开发环境,也就是在Windows上用Visual Studio,Mac上是Xcode,Linux上是gcc。如果你必须在某个平台上用非标准工具写代码,那意味着对你来说写跨平台的代码可能是不值得的。

幸运的是,你可以总是使用标准工具,他们能完美工作。


为什么要花额外的时间和精力实现跨平台

钱!:-) 在Backblaze,数据中心跑Linux是因为Linux免费(当然还有其他原因),数据中心多省一分钱就意味着公司多赚一分钱。同时,世界上90%以上的桌面电脑跑的是windows,为了打入这块市场,我们需要提供一个windows版本的产品。再者,剩下的几乎就是Mac用户了。Mac用户市场贡献了10%的营收,这也是微利产业和厚利产业的差别(译注:大约是指大小两个市场)。

另一个要跨平台的原因是跨平台提高了代码整体质量。每个平台上的编译器都有细微的差别,某个平台的编译器可能会提示一段没有Error的代码有warning,这个有用的warning可能意味着在将来另一个平台上运行崩溃(译注:多个编译器共同编译,减少了warning)。不同平台的调试器和运行时环境也可能不同,所以有时候在Microsoft Visual Studio难住程序员的问题,在另一个平台上就会更快地暴露出本质原因。反之也是如此。

你能够从不同平台的工具中获益。如果要使用gprof工具帮助程序员调试一个性能问题,那么也就是增加一个Linux上的快速编译器标志(quick compiler flag,译注:作者是说在Linux上使用gprof只需改一下编译标志即可,很容易就可以使用gprof工具),但在Windows和Xcode上并不具备。

如果读完上面的内容,你觉得你得在所有的开发环境中都很熟练才行,那么,我会说我们通常只需要在一个小时内就可以让一个windows程序员迁移到Mac或Linux环境(或者相反)。这其中并没有什么可怕的学习曲线。程序员只需要在其他平台上检出(check out)代码树,选择Build All菜单或者是在项目顶级目录下敲入make命令。大部分build的问题都能立刻解决,例如undefined symbol显然是因为平台相关导致的,那就只需要一个快速源码微调(source code tweak)就能搞定。

好,接下来就直接进入这10个跨平台编程规则。


Rule #1: 同时开发 – 不要先开发后移植。不要把迁移工作外包。

当一个工程师设计了一个功能或者修复了一个Bug,他必须一开始就考虑所有的目标平台,并使得代码能够在所有的平台上正常工作。据我估算,同时跨三个平台写C代码开发会比只针对一个平台多花5%的时间,但如果你花费了一整年时间在windows上开发完成,迁再移到Mac上,整个过程可能会花费你近两倍的时间。

说的明白点,我说的“同时开发”是指在写代码前,刚开始设计时就得将所有平台都考虑进去。但是,C代码首先在一个平台上开发(选择一个喜欢的或者工具好用的)。在几个小时内,在某个平台上编写、编译、测试、修改了一段代码后,由同一个程序员在其他平台完成相同的工作。

“先写再迁移”的方法之所以成本比较高,有这么几个原因:

1. 程序员在刚完成一个功能时,他还记得设计准则和边界条件。同时让一个功能在多个平台上正常工作,事实上只需要设计一次,学习曲线也只需要爬一次。要是让同一个程序员开发了一年再迁移,他还得重新熟悉代码。特定的问题和边界条件估计都忘了,还得在测试阶段重新发现。

2. 不采用开发后再迁移的主要原因还是考虑到,只在一个平台上开发,程序员可能会走捷径,或者简单地就忽略了其他平台,或者就懒得担心其他平台了。一个具体的例子是在window下过度使用注册表。注册表实际上是在文件系统中一个特别的位置存放name-value对的API。注册表是个不错的系统,但是注册表基本上与在特定位置存放的XML文件差不多。如果一个程序员不考虑跨平台,那他可能就随意读写注册表,一年后开发完再迁移时,程序员就得重新考虑不支持注册表的平台,然后从头用XML文件实现一遍。如果程序员考虑了跨平台,那一开始就会选择使用XML文件,这种方法就不会再导致其他平台上重写代码了。

3. 最糟糕的的事情莫过于将迁移工作外包给另外的公司或组织。最合适接着改代码的人就是原来写代码的那个程序员,处理跨平台问题最有效率的方法就是直接看代码。如果将迁移工作外包了,等外包完成了,你还得应付沟通交流、代码merge、合并后的代码稳定性问题、不一致的组织目标(?misaligned organizational goals)、不一致的进度等等。外包任何代码几乎都是个错误,外包一个平台移植铁定是个悲剧。

Rule #2:将GUI代码作为不可复用代码,然后为底层逻辑开发跨平台的库

一些工程师认为跨平台意味着“最小公分母程序”(?least common denominator programs)或者“糟糕的移植不能体现我最喜欢的平台特色”。这是不对的。你不应该牺牲任何质量或者平台特色。我们的目标是最大程度重用代码而不牺牲任何终端用户的体验。对于终端用户,大部分软件中最不可重用的代码就是GUI代码。特别是按钮、菜单、弹出对话框、幻灯片窗格等。在windows下,GUI往往在一个“.rc”资源文件中,由Visual Studio对话框编辑器设计产生。在Mac下,GUI通常存放在“。xib”文件中,由Interface Builder设计产生。

编辑GUI反映了各个平台的工具差异。得承认的一点是这些代码通常都要从头重新实现一遍,可能甚至一些布局变化也要重新做。幸运的是GUI也是大部分应用的最简单的部分,通常在一些GUI Builder中拖拖拽拽就行了。


Rule #3: 使用标准C类型,不要使用特定于平台的类型。

这似乎是极其明显的事,但这确实是导致后期越来越多的代码难以修复的最常见的错误之一。我们举个具体的例子。Windows提供了额外的类型DWORD, 定义为"typedef unsigned long DWORD"。显然使用C语言原始的类型" unsigned long"更好,并且原始类型是跨平台的。但使用DWORD而不是原始类型确实是windows程序员常见的错误。这其中的原因可能是特定操作系统的系统调用的返回值是平台相关的类型,然后程序员就接着用这个类型进行进一步的处理了。更甚者,程序员为了一致,可能就使用这个类型声明变量等。但是,相反的是,如果程序员一开始就立刻将平台相关类型转为标准C类型,代码就会保持跨平台。

最该应用这个规则的地方是在跨平台的库。你甚至不能在Mac上调用一个DWORD类型变量为参数的函数,所以参数必须作为unsigned long传递。


Rule #4: 只使用内置的 #ifdef 编译标志,不要自己发明轮子

如果你确实需要实现平台相关代码,你应该把代码包括在标准的#ifdef中。不要自己发明同样功能的代码,额外地在Makefile或build脚本中打开或关闭。

一个好的例子是Visual Studio有一个#ifdef编译标记叫做“_WIN32”,它是windows下C代码中100%可以使用的标记。不要自己发明这类标记(译注:指上文所说的makefile或build脚本中的自己的标记)。你只要这样用就行了:


1

2

3


[color=rgb(184, 92, 0) !important]#ifdef _WIN32
[color=rgb(255, 128, 0) !important]// Microsoft Windows Specific Calls here
[color=rgb(184, 92, 0) !important]#endif



这种情况下,不管你用的是Makefile还是Visual Studio,或者是从别处复制了这段代码,build过程总是正确的。

还有,这条规则虽然很显然,但是很多(大部分?)网络上的自由软件坚持让你正确地设置很多很多编译器标记。如果你借用了这类代码,就可能会导致移植上的困难。


Rule #5: 开发一个简单的可重用的跨平台的基础库,来隐藏每个平台的代码。

我们考虑一个具体的例子。在Backblaze公司的代码中,”BzFile::FileSizeInBytes(const char* fileName)”函数会返回一个文件中有多少字节。在Windows系统中,这个函数实现为windows下特定的一个函数GetFileAttributesEx();在Linux系统下,实现为lstat64(),在Mac系统下;实现为Mac下的特定函数getattrlist()。所以这个函数是一个#ifdef分开的没有共同代码的代码片段。现在所有调用Backblaze的调用者都能正确调用,并且知道这个函数能够快速高效准确地执行,所以这个函数能够在所有平台无缝工作。

实践中,封装常用函数花费时间不多,但这些函数会被调用数百次。所以你应该搞定这些小而简单的功能单元,这会只会拖慢开发一会而已。相信我,你以后会发现很值得。


Rule 6#: 在所有的API中都使用Unicode(特别是UTF-8

本文不是Unicode的教程,所以我只是简单总结一下。现在,Unicode在各个平台所有应用上都是100%支持的。尤其是Unicode的一种UTF-8是一种“正确的选择”。Windows XP、Windows Vista、Macintosh OS X、Linux、Java、C#、所有的主流浏览器,所有主流的email程序(Microsoft Outlook、Outlook Express、Gmail)等所有东西,各种地方,任何时候都支持UTF-8。这绝对不会错。你正在读的这个页面就是UTF-8写的,你的浏览器完美显示,对吧。

赞一下Microsoft,他们率先于1993年Windows NT中就支持Unicode了(15年了,译注:作者写这篇文章是在2008)。然而早期Microsoft的C语言API选择是UTF-16,后来在它们的Java和C# API用的是UTF-8。对那些不懂Unicode的同学,请把UTF-8和UTF-16当做同一底层字符串的两种不同的编码。你可以只用一行代码就能在两中编码之间快速高效互转,且不需要什么外部信息。转换算法很简单,也不会损失信息。

那么~~~(SOOOO,译注:英语里面这种奇怪的东西真不知道怎么翻译),我们来看看Rule #5的BzFile::FileSizeInBytes(const char *fileName)例子。fileName实际上就是UTF-8,所以我们能够支持所有的语言,例如日语:C:\tmp\子犬.txt(译注:这个算是日语~,意为小狗)。

UTF-8的一个好的性质是它向后兼容ASCII码,同时支持所有的国际语言,例如日语。Mac文件系统API调用和Linux文件系统API调用本身已经使用了UTF-8,所以BzFile::FileSizeInBytes在两个平台上的实现很直接。但是windows要做个转换:


1

2

3

4

5

6

7

8

9


[color=rgb(128, 0, 128) !important]int[color=rgb(0, 111, 224) !important] [color=rgb(0, 45, 122) !important]BzFile[color=rgb(0, 111, 224) !important]::[color=rgb(0, 78, 208) !important]FileSizeInBytes[color=rgb(51, 51, 51) !important]([color=rgb(128, 0, 128) !important]const[color=rgb(0, 111, 224) !important] [color=rgb(128, 0, 128) !important]char[color=rgb(0, 111, 224) !important] [color=rgb(0, 111, 224) !important]*[color=rgb(0, 45, 122) !important]fileName[color=rgb(51, 51, 51) !important])
[color=rgb(51, 51, 51) !important]{
[color=rgb(184, 92, 0) !important]#ifdef _WIN32
[color=rgb(0, 111, 224) !important]        [color=rgb(0, 45, 122) !important]wchar[color=rgb(51, 51, 51) !important]_t [color=rgb(0, 111, 224) !important] [color=rgb(0, 45, 122) !important]utf16fileNameForMicrosoft[color=rgb(51, 51, 51) !important][[color=rgb(206, 0, 0) !important]1024[color=rgb(51, 51, 51) !important][color=rgb(51, 51, 51) !important];  [color=rgb(0, 111, 224) !important] [color=rgb(255, 128, 0) !important]// an array of wchar_t is UTF-16 in Microsoft land
[color=rgb(0, 111, 224) !important]    [color=rgb(0, 111, 224) !important] [color=rgb(0, 78, 208) !important]ConvertUtf8toUtf16[color=rgb(51, 51, 51) !important]([color=rgb(0, 45, 122) !important]fileName[color=rgb(51, 51, 51) !important],[color=rgb(0, 111, 224) !important] [color=rgb(0, 45, 122) !important]utf16fileNameForMicrosoft[color=rgb(51, 51, 51) !important])[color=rgb(51, 51, 51) !important]; [color=rgb(0, 111, 224) !important] [color=rgb(255, 128, 0) !important]// convert from Utf8 to Microsoft land!!
[color=rgb(0, 111, 224) !important]   [color=rgb(0, 111, 224) !important] [color=rgb(0, 78, 208) !important]GetFileAttributesEx[color=rgb(51, 51, 51) !important]([color=rgb(0, 45, 122) !important]utf16fileNameForMicrosoft[color=rgb(51, 51, 51) !important],[color=rgb(0, 111, 224) !important] [color=rgb(0, 45, 122) !important]GetFileExInfoStandard[color=rgb(51, 51, 51) !important],[color=rgb(0, 111, 224) !important] [color=rgb(0, 111, 224) !important]&[color=rgb(0, 45, 122) !important]fileAttr[color=rgb(51, 51, 51) !important])[color=rgb(51, 51, 51) !important];
[color=rgb(0, 111, 224) !important]     [color=rgb(128, 0, 128) !important]return[color=rgb(0, 111, 224) !important] [color=rgb(51, 51, 51) !important]([color=rgb(0, 45, 122) !important]win32fileInfo[color=rgb(51, 51, 51) !important].[color=rgb(0, 45, 122) !important]nFileSizeLow[color=rgb(51, 51, 51) !important])[color=rgb(51, 51, 51) !important];
[color=rgb(184, 92, 0) !important]#endif
[color=rgb(51, 51, 51) !important]}



上述代码只是示例(有缓冲器溢出的风险),也不能处理大于2GB的文件,但从中你能知道大概的思路。最重要的是意识到只要你在开始整个项目前考虑使用UTF-8,然后支持跨平台就会很简单直接了。要不然,你一开始用Microsoft的UTF-16,工作了一年之后再移植到其他平台上,简直是个噩梦。


Rule #7: 不要使用第三方应用程序框架或者运行时环境来是你的代码跨平台。

第三方库能提供你所需要的很有用的功能。例如,我强烈建议OpenSSL,它免费、可重新发布、超级安全(译注:原文写于08年,那时还没曝出HeartBleed漏洞)、快速加密,Backblaze自己实现一个,也不可能写的比OpenSSL好。但是如果你要使用第三方库来支持跨平台而不是提供功能性,那就是另一回事了。本文的前提是C/C++本身是跨平台的(译注:应指C语言标准之类),你不需要应用程序框架来使C语言跨平台。对于GUI程序尤其如此(参见Rule #2)。如果你使用了一个声称是跨平台的GUI层,你多半只会得到一个丑陋且功能不全的GUI。

有很多应用程序框架声称能够节省你的时间,但最后它们只是限制了你的功能。它们的学习曲线也让从它们获取的好处黯然失色(eclipse)。这里是几个例子:Qt (by TrollTech), ZooLib, GLUI/GLUT, CPLAT, GTK+, JAPI, etc。

在最糟糕的案例中,这些应用程序框架让你的应用程序变得臃肿,你的程序得带着这些额外的库和运行时环境。实际上这往往会导致额外的兼容性问题,例如这些运行时环境没有安装或者更新怎么办,你的程序就陪着它们不能安装了,或者在某些平台上运行不正常。

聪明的读者可能会发现,这和Rule #5冲突。其实关键是你确实应该开发你自己的跨平台的基础库,而不是尝试使用别人的。这虽然和代码重用的想法冲突,事实上,你只需要花费少量时间封装少量的你真正需要的调用接口。你花费少量时间,获得的是对你自己的基础库的全面的控制。为了使一切透明一些,这些都是值得的。这其实很简单的。


Rule #8: 原生代码本来就总是可以所有平台上编译->不是某个脚本让它们从不能编译变得能够编译了

相同的foo.cpp和foo.h文件能够在Windows、Mac、Linux上被检出并构建。我好奇的是没有这并没有被理解和体现。但是如果要编译OpenSSL源码,你要运行一个perl脚本,然后才能Build(译注:if you compile OpenSSL there is a complex dance where you run a PERL script on the source code, THEN you build it,根据上下文,作者应该是说OpenSSL编译比较麻烦,还得用perl脚本)。不要误解我,我真的喜欢OpenSSL,它具备神奇的功能,免费,花点时间就可以在所有平台上构建。我只是不理解OpenSSL作者为什么不预先在源码上执行perl脚本,这样就能直接编译了。

这篇文章的全部观点就是如何写出跨平台的代码。我相信上面说的perl脚本会导致码农编译过程中有麻烦,perl脚本也可能隐藏了错误。从头写代码吧,你不需要perl脚本。


Rule #9:所有的程序都能在所有的平台编译

教会一个菜鸟程序员在一个他从未用过的平台上检出并构建代码只花费10分钟。你在名片的空白面写出四五步指令,然后贴在菜鸟的显示器上。如果这个菜鸟程序员按照本文的这些规则写代码,那么他的代码就能够无缝跨平台。这不是什么大负担。所有的程序员必须对自己的代码能够在所有平台编译负责,要不然整体进度不会顺利。

小的开发团队(小于20个程序员),用版本控制工具(subversion,CVS)作为多个平台构建的同步点就可以。我是说,一个新功能在一个平台上开发测试完成后,check in到Subversion,然后立刻在其他平台check out并编译。这意味着构建过程可能会因为更正小疏忽而打断一两分钟。你可能会认为其他程序员通常也能够在某天花费一两分钟就注意到这些小疏忽,但你大错特错了。据我15年来带10~50个程序员的团队经验,在被打断的情况下,我碰巧还能知道代码树,花费了5分钟后我又能继续的情况,我记得的只有两次。

在这15年里,有几百次的情况是:构建过程的出现的问题跟跨平台一点关系也没有,而是跟顽固或者能力不济的程序员,尤其是不遵循Rule #9所说的所有程序在所有平台编译规则的程序员有关。这一点让我有了下面的最后一条规则。


Rule #10:开除那些懒惰、不称职、态度差、不遵循这些规则的程序员

有时候在某个平台上构建过程会出错,这没什么,只要不是经常出错就好。团队中最好的程序员也会在check in代码的时候忘记测试其他平台上的测试。但是如果团队中某个程序员一直在跨平台构建中出错,那该开除这个程序员了。一旦团队确定了跨平台开发交付的目标,程序员忽略这些规则导致系统问题就是不够职业了。

或者这个烦人的程序员是真的不称职,或者他是极度疯狂,不管怎么样,这不只是开除的理由,我觉得这还是道德义务。这样的程序员给那些认真工作的好程序员带来了坏名声。这意味着我们自豪的同业人员竟然允许差程序员继续产生差劲的代码。

结论:跨平台很简单,但是你得从第一行代码就开始做起。

本文最重要的一点就是如果你曾想过让你的项目跨平台,那么你必须从一开始就考虑这个问题。如果你遵循了这个哲学原理,剩下那些就自然而然的出来了。Backblaze就是这种情况。


原文链接:

http://www.ski-epic.com/source_code_essays/ten_rules_for_writing_cross_platform_c_source_code.html


http://loopjump.com/ten_rules_for_cross_platform_cpp/




回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|firemail ( 粤ICP备15085507号-1 )

GMT+8, 2024-11-23 10:15 , Processed in 0.067699 second(s), 19 queries .

Powered by Discuz! X3

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表