分类 软件开发 下的文章

Java 仍是开发企业应用程序最流行的语言之一。那么,为什么 无服务器 serverless 开发人员对它望而却步?

 title=

几十年来,企业已经在各类平台上开发了关键业务应用程序,包括物理服务器、虚拟机和云环境。这些应用程序在各行各业都有一个共同点,那就是无论需求如何,它们都需要持续可用(24x7x365),保证稳定性、可靠性和性能。因此,即使实际资源利用率低于 50%,每个企业都必须付出高额成本维护基础架构(如 CPU、内存、磁盘、网络等)。

无服务器架构是为了帮助解决这些问题而产生的。无服务器允许开发人员按需构建和运行应用程序,保证高可用性,不必在多云和混合云环境中管理服务器。在幕后,无服务器拓扑中仍有很多服务器,但它们是从应用程序开发中抽象出来的。相反,云提供商使用无服务器进行资源管理,例如配置、维护、联网和扩展服务器实例。

由于其高效性,无服务器开发模式现在是一些企业的需求,这些企业希望按需启动服务,而不是一直运行服务。

许多新建的开源项目用来在 Kubernetes 集群上通过 Linux 容器包来管理无服务器应用程序。CNCF 的《交互式无服务器全景》 是一份关于开源项目、工具、框架和公共云平台的指南,帮助 DevOps 团队处理无服务器应用程序。

 title=

开发人员可以编写代码,然后将其快速部署到各种无服务器环境中。然后,无服务器应用程序响应需求,并根据需要自动伸缩扩展。

你可能想知道什么编程语言和运行环境最适合无服务器应用程序开发,以便与上图中的技术集成。这个问题不只一个答案,但是让我们退一步来讨论在企业生产环境中开发业务应用程序最流行的应用程序运行环境:Java。

Developer Economics 称,截至 2020 年第三季度,仍有 800 多万家企业开发人员在使用 Java 来实现其业务需求。然而,根据 2020 年的一项调查,Java(占比 6%)显然不是有前瞻意识的开发人员的首选,他们使用当前云服务做开发。

 title=

来自 NewRelic 无服务器基准报告的数据(Daniel Oh, CC BY-SA 4.0

资源使用、响应时间和延迟在无服务器开发中至关重要。公有云提供商提供的无服务器产品通常是按需计量的,只有在无服务器应用程序启动时,才会通过事件驱动的执行模式收费。因此,当无服务器应用程序闲置或缩减为零时,企业无需支付任何费用。

带有容器的 Java 状态

在这种背景下,你可能会问:“既然现有业务应用程序很可能是在 Java 技术上开发的,那么开发人员为什么不尝试使用 Java 栈进行无服务器应用程序开发呢?

隐藏的真相是:很难在新的不可变更的基础设施(例如 Kubernetes 这样的容器平台)中优化 Java 应用程序。

 title=

该图描述了 Java 进程与竞争的语言、框架(如 Node.jsGo)之间内存资源使用的差异。Java HotSpot 占用资源最大,其中包括每个 Java 虚拟机 Java Virtual Machine (JVM)实例分配的堆内存。中间显示了 Node.js 每个进程要比 Java 小得多。最后,Go 是一种流行的云服务编程语言,因为它的内存消耗最低。

如你所见,当你在这张图从左到右走,你会看到更密的节点。这就是开发人员在云、容器和 Kubernetes 上编写无服务器应用程序时回避 Java(包括 Spring Boot,一种顽固的微服务 Java 框架)的原因。

下一步是什么?

企业可以通过实现无服务器应用程序获得明显的好处,但是资源密度问题导致他们避免使用 Java 堆栈在 Kubernetes 上开发无服务器应用程序开发。但是选择其他语言会给全球数百万 Java 开发人员带来学习负担。因此,在本系列的下一篇文章中,我将指导你如何开始使用 Java 无服务器函数,而不是使用其他语言。


via: https://opensource.com/article/21/5/what-serverless-java

作者:Daniel Oh 选题:lujun9972 译者:DCOLIVERSUN 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

JavaScript 前端工具的生态系统充满着变数和竞争,且只有最好的工具才会存活下来。

 title=

生产中使用的代码与开发中的有所不同. 在生产中,我们需要构建一些能运行得够快、能管理各种依赖关系、能自动执行任务、能加载外部模块等功能的包。而那些将开发中的代码转为生产代码的 JavaScript 工具我们就称之为 构建工具。

我们可以通过各个构建步骤以及其重要性来解释前端代码需要被“构建”的原因。

前端代码构建步骤

前端代码的构建涉及下面的四个步骤:

1、转译

通过 转译 Transpiling ,开发者可以使用到语言最新、最热门的更新和扩展,并保持浏览器的兼容性等。下面是使用 Babel 的一个例子:

// 数组映射中的箭头函数语法
const double = [1, 2, 3].map((num) => num * 2);
// 转译后
const double = [1, 2, 3].map(function(num) {
  return num * 2;
});

2、分包

分包 Bundling 是处理所有 importrequire 语句的过程;找到相匹配的 JavaScript 代码片段、包和库;将它们添加到适当的域中;然后将它们打包到一个大的 JavaScript 文件中。常用的分包器包括 Browserify、Webpack 与 Parcel。

3、压缩

压缩 Minifing 是通过删除空白和代码注释来减少最终的文件大小。在压缩过程中,我们还可以更进一步添加代码混淆步骤,混淆会更改变量名和方法名,使代码变得晦涩难懂,因此一旦代码交付到客户端,它就不是那么容易能让人读懂。下面是一个使用 Grunt 的例子:

// 压缩前
const double = [1, 2, 3].map(function(num) {
  return num * 2;
});
// 压缩后
const double=[1,2,3].map(function(num){return num*2;});

4、打包

完成上面的所有步骤之后, 我们需要将这些具有兼容性、且经过分包、压缩/混淆过的文件放置到某个地方。 打包 Packaging 正是这样一个过程,它将上述步骤所产生的结果放置到开发者指定的某个位置上,这通常是通过打包器完成的。

前端构建工具

前端工具及构建工具可以分为以下几类:

  • 包管理: NPM、Yarn
  • 转译器: Babel 等
  • 打包器: Webpack、Parcel、Browserify
  • 压缩混淆: UglifyJS、Packer、Minify 等

JavaScript 生态系统中有各种各样的构建工具可以使用,包括下面的这些:

Grunt 和 Bower

Grunt 是作为命令行工具引入的,它仅提供一个脚本来指定和配置相关构建任务。Bower 作为包管理器,提供了一种客户端包的管理方法而紧追其后。这两者,再加上 NPM,它们经常在一起使用,它们看上去似乎可以满足大多数的自动化需求,但 Grunt 的问题在于它无法提供给开发者配置更复杂任务的自由,而 Bower 使开发者管理的程序包是平常的两倍,因为它将前端包、后台包分开了(例如,Bower 组件与 Node 模块)。

Grunt 与 Bower 的未来: Grunt 与 Bower 正在退出 JavaScript 工具生态,但是还有一些替代品。

Gulp 和 Browserify

Gulp 是在 Grunt 发布一年半之后才发布的。但 Gulp 却让大家感到很自然、舒服。用 JavaScript 来写构建脚本与用 JSON 来写相比更自由。你可以在 Gulp 的构建脚本中编写函数、即时创建变量、在任何地方使用条件语句 —— 但就这些,并不能说让我们的感觉变得特别自然和舒适,只能说这只是其中的一个可能的原因。Browserify 和 Gulp 可以配合使用,Browserify 允许 NPM 包(用于后端 Node 服务器)被直接带入到前端,就这一点已经直接让 Bower 废了。而正是这种用一个包管理器来处理前后端包的方式让人感到更自然和更好。

Gulp 的未来: Gulp 可能会被改进,以便匹配当前流行的构建工具,但这完全取决于创造者的意愿。Gulp 仍在使用中,只是不再像以前那么流行了。

Webpack 和 NPM/Yarn 脚本

Webpack 是现代前端开发工具中最热门的宠儿,它是一个开源的 JavaScript 模块打包器。Webpack 主要是为处理 JavaScript 而创造的,但如果包含相应的加载器,它也可以转换 HTML、CSS 和图片等前端资源。通过 Webpack,你也可以像 Gulp 一样编写构建脚本,并通过 NPM/Yarn 来执行它们。

Webpack 的未来: Webpack 是目前 JavaScript 工具生态系统中最热门的工具,最近几乎所有的 JavaScript 库都在使用 React 和 Webpack。Webpack 目前处于第四个版本,不会很快消失。(LCTT 译注:Webpack 目前已经发布了第五个版本了,且还在火热更新中)

Parcel

Parcel 是一个 Web 应用打包器,于 2018 年推出,因其开发者体验而与众不同。Parcel 能利用处理器多核功能提供极快的打包性能,且还零配置。但 Parcel 还是一个新星,对于一些大型应用,其采用率并不高。相比之下,开发人员更喜欢使用 Webpack,因为 Webpack 有更广泛的支持和可定制性。

Parcel 的未来: Parcel 非常容易使用,如果你统计打包和构建时间,它会比 Webpack 更快,而且它还提供了更好的开发者体验。Parcel 没有被大量采用的原因可能是它仍然比较新。在前端构建工具的生态系统中,Parcel 的前景会非常光明,它将会存在一段时间。

Rollup

Rollup 是 JavaScript 的一个模块分包器,它可将一小段代码编译为更大更复杂的库或应用。Rollup 一般建议用来构建 JavaScript 库,特别是那种导入和依赖的第三方库较少的那种库。

Rollup 的未来: Rollup 很酷,且正在被迅速采用。它有很多强大的功能,将在很长一段时间内作为前端工具生态系统的一个组成部分而存在。

了解更多

JavaScript 前端工具的生态系统充满着变数和竞争,且只有最好的工具才能存活下来。在不久的将来,我们的构建工具将具有更少(或没有)的配置,更方便的定制化,更好的扩展性的和更好的构建速度。

该用什么样的构建工具用于你的前端项目,你需要根据具体的项目需求来做出决定。至于选择什么样的工具,才是最合适自己的,大多数时候,需要我们自己作出取舍。

更多信息, 请看:

这篇文章最初发表在 Shedrack Akintayo 的博客 上,经许可后重新发表。


via: https://opensource.com/article/20/11/javascript-build-tools

作者:Shedrack Akintayo 选题:lujun9972 译者:ywxgod 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

比较 Git 中四种切换分支的方法的优缺点。

 title=

所有大量使用 Git 的人都会用到某种形式的上下文切换。有时这只会给你的工作流程增加少量的开销,但有时,这可能是一段痛苦的经历。

让我们用以下这个例子来讨论一些常见的上下文切换策略的优缺点:

假设你在一个名为 feature-X 的分支中工作。你刚刚发现你需要解决一个无关的问题。这不能在 feature-X 分支中完成。你需要在一个新的分支 feature-Y 中完成这项工作。

方案 1:暂存 + 分支

解决此问题最常见的工作流程可能如下所示:

  1. 停止分支 feature-X 上的工作
  2. git stash
  3. git checkout -b feature-Y origin/main
  4. 一顿鼓捣,解决 feature-Y 的问题
  5. git checkout feature-Xgit switch -
  6. git stash pop
  7. 继续在 feature-X 中工作

优点: 这种方法的优点在于,对于简单的更改,这是一个相当简单的工作流程。它可以很好地工作,特别是对于小型仓库。

缺点: 使用此工作流程时,一次只能有一个工作区。另外,根据你的仓库的状态,使用暂存是一个麻烦的环节。

方案 2:WIP 提交 + 分支

这个解决方案和前一个非常相似,但是它使用 WIP( 正在进行的工作 Work in Progress )提交而不是暂存。当你准备好切换回来,而不是弹出暂存时,git reset HEAD~1 会展开 WIP 提交,你可以自由地继续,就像之前的方案一样,但不会触及暂存。

  1. 停止分支 feature-X 上的工作
  2. git add -u(仅仅添加修改和删除的文件)
  3. git commit -m "WIP"
  4. git checkout -b feature-Y origin/master
  5. 一顿鼓捣,解决 feature-Y 的问题
  6. git checkout feature-Xgit switch -
  7. git reset HEAD~1

优点: 对于简单的更改,这是一个简单的工作流,也适合于小型仓库。你不需要使用暂存。

缺点: 任何时候都只能有一个工作区。此外,如果你或你的代码审阅者不够谨慎,WIP 提交可能会合并到最终产品。

使用此工作流时,你永远不要想着将 --hard 添加到 git reset。如果你不小心这样做了,你应该能够使用 git reflog 恢复提交,但是你最好完全避免这种情况发生,否则你会听到心碎的声音。

方案 3:克隆一个新仓库

在这个解决方案中,不是创建新的分支,而是为每个新的功能分支创建存储库的新克隆。

优点: 你可以同时在多个工作区中工作。你不需要 git stash 或者是 WIP 提交。

缺点: 需要考虑仓库的大小,因为这可能会占用大量磁盘空间(浅层克隆可以帮助解决这种情况,但它们可能并不总是很合适。)此外,你的仓库克隆将互不可知。因为他们不能互相追踪,所以你必须手动追踪你的克隆的源仓库。如果需要 git 钩子,则需要为每个新克隆设置它们。

方案 4:git 工作树

要使用此解决方案,你可能需要了解 git add worktree。如果你不熟悉 Git 中的工作树,请不要难过。许多人多年来都对这个概念一无所知。

什么是工作树?

将工作树视为仓库中属于项目的文件。本质上,这是一种工作区。你可能没有意识到你已经在使用工作树了。开始使用 Git 时,你将自动获得第一个工作树。

$ mkdir /tmp/foo && cd /tmp/foo
$ git init
$ git worktree list
/tmp  0000000 [master]

你可以在以上代码看到,甚至在第一次提交前你就有了一个工作树。接下来去尝试再添加一个工作树到你的项目中吧。

添加一个工作树

想要添加一个新的工作树你需要提供:

  1. 硬盘上的一个位置
  2. 一个分支名
  3. 添加哪些分支
$ git clone https://github.com/oalders/http-browserdetect.git
$ cd http-browserdetect/
$ git worktree list
/Users/olaf/http-browserdetect  90772ae [master]

$ git worktree add ~/trees/oalders/feature-X -b oalders/feature-X origin/master
$ git worktree add ~/trees/oalders/feature-Y -b oalders/feature-Y e9df3c555e96b3f1

$ git worktree list
/Users/olaf/http-browserdetect       90772ae [master]
/Users/olaf/trees/oalders/feature-X  90772ae [oalders/feature-X]
/Users/olaf/trees/oalders/feature-Y  e9df3c5 [oalders/feature-Y]

与大多数其他 Git 命令一样,你需要在仓库路径下使用此命令。一旦创建了工作树,就有了隔离的工作环境。Git 仓库会跟踪工作树在磁盘上的位置。如果 Git 钩子已经在父仓库中设置好了,那么它们也可以在工作树中使用。

请注意到,每个工作树只使用父仓库磁盘空间的一小部分。在这种情况下,工作树需要只大约三分之一的原始磁盘空间。这这非常适合进行扩展。如果你的仓库达到了千兆字节的级别,你就会真正体会到工作树对硬盘空间的节省。

$ du -sh /Users/olaf/http-browserdetect
2.9M

$ du -sh /Users/olaf/trees/oalders/feature-X
1.0M

优点: 你可以同时在多个工作区中工作。你不需要使用暂存。Git 会跟踪所有的工作树。你不需要设置 Git 钩子。这也比 git clone 更快,并且可以节省网络流量,因为你可以在飞行模式下执行此操作。你还可以更高效地使用磁盘空间,而无需借助于浅层克隆。

缺点: 这是个需要你额外学习和记忆的新东西,但是如果你能养成使用这个功能的习惯,它会给你丰厚的回报。

额外的小技巧

有很多方式可以清除工作树,最受欢迎的方式是使用 Git 来移除工作树:

git worktree remove /Users/olaf/trees/oalders/feature-X

如果你喜欢 RM 大法,你也可以用 rm -rf 来删除工作树。

rm -rf /Users/olaf/trees/oalders/feature-X

但是,如果执行此操作,则可能需要使用 git worktree prune 清理所有剩余的文件。或者你现在可以跳过清理,这将在将来的某个时候通过 git gc 自行完成。

注意事项

如果你准备尝试 git worktree,请记住以下几点:

  • 删除工作树并不会删除该分支。
  • 可以在工作树中切换分支。
  • 你不能在多个工作树中同时签出同一个分支。
  • 像其他命令一样,git worktree 需要从仓库内运行。
  • 你可以同时拥有许多工作树。
  • 要从同一个本地仓库签出创建工作树,否则它们将互不可知。

git rev-parse

最后一点注意:在使用 git worktree 时,仓库根所在的位置可能取决于上下文。幸运的是,git rev parse 可以让你区分这两者。

  • 要查找父仓库的根目录,请执行以下操作:
git rev-parse --git-common-dir
  • 要查找你当前所在仓库的根目录,请执行:
git rev-parse --show-toplevel

根据你的需要选择最好的方法

就像很多事情一样,TIMTOWDI( 条条大道通罗马 there's more than one way to do it )。重要的是你要找到一个适合你需要的工作流程。你的需求可能因手头的问题而异。也许你偶尔会发现自己将 git worktree 作为版本控制工具箱中的一个方便工具。


via: https://opensource.com/article/21/4/context-switching-git

作者:Olaf Alders 选题:lujun9972 译者:Chao-zhi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

CMake 是一个跨平台的编译、测试和打包软件,即使你以前从来没有使用过构建系统,也可以轻松上手。

 title=

在我以前的文章 Autotools 入门 一文中,我说明了如何使用 Autotools 来管理和打包代码。这是一个强大且通用的平台,可轻松集成到许多打包系统中,包括 RPM、APT、pkgsrc 等等。它的语法和结构可能会令人困惑,但幸运的是,我们还有其他选择,开源的 CMake 就是其中一个。

CMake 是一个用于构建、测试和打包软件的跨平台套件。它使用简单而清晰的语法,因此即使你以前从未使用过构建系统,也很容易开始使用。

安装 CMake

CMake 可能已经安装在你的 Linux 系统上。如果没有,你可以使用发行版的程序包管理器进行安装:

$ sudo dnf install cmake

在 Debian 或者其他相似的系统上:

$ sudo apt install cmake

在 Mac 上,你可以使用 MacPorts 或者 Homebrew 来安装:

$ sudo port install cmake

在 Windows 上,你可以使用 Chocolatey 或者直接从 CMake 网站 下载二进制来安装。

使用 CMake

对于想要从源代码构建软件的开发人员或用户来说,CMake 是一种快速简便的编译和安装方法。 CMake 分阶段工作:

  1. 首先,在 cmake 步骤中,CMake 扫描计算机查看一些默认设置。默认设置包括库的位置以及在系统上安装软件的位置。
  2. 接下来,使用系统上的 make 命令(在 Linux 上是 GUN Make,在 NetBSD 上是 NetBSD Make)来编译程序。这个过程通常是将人类可读的源代码转换成机器语言。
  3. 最后,在 make install 一步中,那些编译过的文件将被拷贝到(在 cmake 步骤中扫描出来的)计算机上合适的位置。

这看起来很简单,当你使用 CMake 时就是这样。

CMake 的可移植性

CMake 在设计时就考虑了可移植性。虽然它不能使你的项目在所有 POSIX 平台上都能正常工作(这取决于作为开发者的你),但它可以确保将标记为要安装的文件安装到已知平台上最合适的位置。而且由于有了 CMake 之类的工具,对于高级用户而言,根据其系统需求自定义和覆盖任何不合适的选项都很容易。

使用 CMake,你只需要知道将哪些文件安装到哪个常规位置即可。它会照顾其他一切。不再需要自定义安装脚本,它们有可能在任何未经测试的操作系统上失败。

打包

像 Autotools 一样,CMake 也得到了很好的打包支持。无论它们是打包成 RPM 还是 DEB 或 TGZ(或其他任何东西),将带有 CMake 的项目交给打包者,他们的工作既简单又直接。打包工具支持 CMake,因此可能不需要进行任何修补或者调整。在许多情况下,可以自动将 CMake 项目整合到工作流中。

如何使用 CMake

要在项目中使用 CMake,只需在项目目录中创建 CMakeLists.txt 文件。首先,声明最低要求的 CMake 版本以及项目名称和版本。CMake 会努力在尽可能长时间内保持兼容性,但是随着你使用的时间越长,并且关注它最新的开发动态,你就会知道哪些特性是你所依赖的。

cmake_minimum_required(VERSION 3.10)

project(Hello VERSION 1.0)

如你可能已经看到的那样,CMake 的语法是一个带有括号和参数的命令。大写的 VERSION 字符串不是任意的,也不只是格式。它们是 project 命令中的有效参数。

在继续之前,先写一个简单的 C 或者 C++ 的 hello world 程序。为了简单,我就写了六行 C 代码,并把它保存在 hello.c 中(为了匹配我在 CMakeLists.txt 中可执行文件的名字)。

#include <stdio.h>

int main() {
   printf("Hello open source\n");
   return 0;
}

不过,不要搞错了,CMake 不仅适用于 C 和 C++。它可以处理任意文件,并且有许多可用的命令,因此它可以帮助你维护许多不同形式的项目。

CMake 网站中记录了所有有效的内置命令及其可用参数,因此无论你要做什么,都可以轻松发现所需的功能。不过,这是一个简单的示例,因此,你需要的下一个命令是必不可少的 —— 你必须为 CMake 定义要构建的代码:

add_executable(Hello hello.c)

这个命令指定了你编译后的二进制文件的名字为 Hello。因此,它与你在终端中执行带有 -o Hellogcc 命令是一样的。

在一些比较复杂的项目中,你可能还需要使用库文件,你可以使用 add library 命令来链接库文件。

在你设置了你想要构建和标记为安装的文件之后,你必须要告诉 CMake 一旦用户安装了程序,最终的应用程序应该在哪个位置。

在这个简单的例子里,你仅需要做的一件事就是在你的 CMakeLists.txt 文件里添加 install 命令。install 命令接受几个参数。但是在这个例子中,你仅需要使用 TARGET 命令来指定你要安装文件的名字。

install(TARGETS Hello)

向 CMake 工程添加一些文件

一个软件项目向用户交付的往往不仅仅只有代码,还有一些其他的文件数据,例如手册或者是信息页、示例项目,或者是配置文件。你可以使用与包含编译文件时类似的工作流程,将任意数据包含在 CMake 项目中:在 CMakelists.txt 文件中使用 file 命令,然后说明一下这些文件要安装在哪里。

例如,你可以在这个项目中包含一个 assets 目录,你可以使用 file 命令,后面跟上 COPYDESTINATION 参数来告诉 CMake 将这些额外的文件复制到你的分发包中。

file(COPY assets DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")

这个 ${CMAKE_CURRENT_BINARY_DIR} 变量是一个特殊的 CMake 内置变量,表示 CMake 正在处理的目录。换句话说,你的任何文件都会被复制到编译目录(在你运行 cmake 命令后,这个过程会更加清晰,到时候回过头来看一下)。

因为这些额外的数据文件有些杂乱不堪(如果你不信的话,可以看一下 /usr/share 这个目录)。对于你自己的项目创建一个子文件夹对谁都有好处。最好也带上版本名字。你可以通过在 CMAKE_CURRENT_BINARY_DIR 中指定一个新的目录,使用你选择的项目名称,后面跟一个为你的项目命名的特殊变量和你在项目声明中为它设置的 VERSION

file(COPY assets DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Hello-${Hello_VERSION}")

定义安装位置

你已经定义你要编译的文件,因此现在你要告诉 CMake 你的程序要安装在哪个位置。比如你的主程序,这个要程使用 install 命令:

install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/Hello-${Hello_VERSION}" TYPE DATA)

这里有一些新的参数。DIRECTORY 参数指定了数据文件是一个目录,而不是一个文件(FILE)或者脚本(SCRIPT)。你使用的参数和复制一些额外文件到编译目录时是一样。另外,在 install 命令中 TYPE 或者 DESTINATION 必须要指定其一。TYPE 参数指定了通用的文件类型,这些文件通常将会被放到合适的位置。在 Linux 系统上,TYPE DATA 一般是 /usr/local/share 或者 /usr/share,除非用户定义了其他的位置。

这是诸如 CMake 之类的良好构建系统的强大功能之一。你不必担心文件的确切位置,因为你知道用户可以更改 CMake 的首选默认设置,并且 CMake 将构建代码以使其正常工作。

运行 CMake

CMake 有多种方式来让你执行命令,你可以在终端或者在一个可交互的程序上执行命令,或者你也可以使用它的图形界面(GUI)。我比较偏向于使用终端命令,但是我也喜欢使用一些其他的方式(相比与在 Makefile 中查找那些晦涩的变量然后去修改它们更胜一筹)。

对于编译过开源 C++ 项目的任何人,都熟悉的第一步是创建一个 build 目录,进入到该目录,然后运行 cmake .. 命令。 我是一个懒惰的打字员,所以我将构建目录命名为 b,但是你可以使用最合适的方式:

$ mkdir b
$ cd b
$ cmake ..
-- The C compiler identification is GNU 11.1.1
-- The CXX compiler identification is GNU 11.1.1
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /var/home/seth/demo-hello/b
$

这或多或少相当于经典的 ./configure; make; make install 中的 ./configure。看一下你的构建目录,CMake 已经帮你生成了几个新的文件,来让你的项目更完整。这里生成了 CMake 的数据文件、一个常规的 Makefile 文件(这是一个免费提供的 247 行的文件,但对于越复杂的项目,行数要多得多),还有一个包含这个示例程序的任意非编译数据的 Hello-1.0 目录。

$ ls
CMakeCache.txt
CMakeFiles
Makefile
Hello-1.0
cmake_install.cmake

接下来,你可以进行构建。你可以使用 CMake 的 --build 选项来做这件事,使用当前的构建目录作为源目录。

$ cmake --build .
Scanning dependencies of target Hello
[ 50%] Building C object CMakeFiles/Hello.dir/hello.c.o
[100%] Linking C executable Hello
[100%] Built target Hello

或者你可以运行 make 命令。这将读取由 CMake 生成的 Makefile 文件。在这个例子中,make 默认的行为就是由源程序 hello.c 生成目标文件。

$ make
Scanning dependencies of target Hello
[ 50%] Building C object CMakeFiles/Hello.dir/hello.c.o
[100%] Linking C executable Hello
[100%] Built target Hello
$

如你所料,Hello 二进制可执行文件现在存在于当前的构建目录中。因为它是一个简单的自包含应用程序,所以你可以运行它进行测试:

$ ./Hello
Hello open source
$

最后,你可以用 --install 选项进行安装。因为我不希望我的简单的 “hello world” 应用程序真的被安装到我的系统上,我设置了 --prefix 选项,将 CMake 的目标从根目录(/)重定向到 /tmp 的一个子目录。

$ cmake --install . --prefix /tmp/hello/
-- Install configuration: ""
-- Installing: /tmp/dist-hello/usr/local/bin/Hello
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file1

另外,你也可以运行 make install 来调用 Makefile 的安装动作。同样,为了避免在我的系统上安装一个演示程序,我在这个例子中设置了 DESTDIR 变量,将安装目标重定向到 /tmp 的一个子目录:

$ mkdir /tmp/dist-hello
$ make install DESTDIR=/tmp/dist-hello
[100%] Built target Hello
Install the project...
-- Install configuration: ""
-- Installing: /tmp/dist-hello/usr/local/bin/Hello
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file1

看一下输出的内容,来确定它具体的安装位置,这个程序已经安装好了。

快速自定义

CMake 的安装前缀(由 CMAKE_INSTALL_PREFIX 变量指定)默认是在 /usr/local 这个位置,但是所有的 CMake 变量都可以在你运行 cmake 命令的时候,加一个 -D 选项来改变它。

$ cmake -DCMAKE_INSTALL_PREFIX=/usr ..
$ make install DESTDIR=/tmp/dist-hello
$ make install DESTDIR=/tmp/dist-hello
[100%] Built target Hello
Install the project...
-- Install configuration: ""
-- Installing: /tmp/dist-hello/usr/bin/Hello
-- Installing: /tmp/dist-hello/usr/share/Hello-1.0
-- Installing: /tmp/dist-hello/usr/share/Hello-1.0/assets/file0
-- Installing: /tmp/dist-hello/usr/share/Hello-1.0/assets/file1

所有由 CMake 使用的变量都可以通过这种方式来修改。

交互式的 CMake

CMake 的交互模式是一种用于配置安装环境的友好而有用的方法。要让用户知道该项目使用的所有可能的 CMake 变量是一件工作量很大的事,因此 CMake 交互式界面是他们无需查看 MakefileCMakeLists 即可发现自定义选项的简便方法。

为了调用这个交互式的 CMake,使用 ccmake 命令,在这个简单的项目里没有太多的东西。但是对于像 Rosegarden 这样的大型项目,这将非常有用。

 title=

CMake 的更多知识

还有很多很多的 CMake 知识需要去了解。作为一个开发者,我非常喜欢它简洁的语法、详尽的文档、可扩展性以及便捷性。作为一个用户我非常喜欢 CMake 友好且实用的错误提示信息还有它的用户界面,如果你的项目还未开始使用构建系统,请了解一下 CMake 吧。你以及以后尝试打包你应用程序的任何人都不会后悔。


via: https://opensource.com/article/21/5/cmake

作者:Seth Kenlon 选题:lujun9972 译者:amwps290 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

软件库是重复使用代码的一种简单而合理的方式。

 title=

软件库是一种是一直以来长期存在的、简单合理的复用代码的方式。这篇文章解释了如何从头开始构建库并使得其可用。尽管这两个示例库都以 Linux 为例,但创建、发布和使用这些库的步骤也可以应用于其它类 Unix 系统。

这些示例库使用 C 语言编写,非常适合该任务。Linux 内核大部分由 C 语言和少量汇编语言编写(Windows 和 Linux 的表亲如 macOS 也是如此)。用于输入/输出、网络、字符串处理、数学、安全、数据编码等的标准系统库等主要由 C 语言编写。所以使用 C 语言编写库就是使用 Linux 的原生语言来编写。除此之外,C 语言的性能也在一众高级语言中鹤立鸡群。

还有两个来访问这些库的示例 客户程序 client (一个使用 C,另一个使用 Python)。毫无疑问可以使用 C 语言客户程序来访问 C 语言编写的库,但是 Python 客户程序示例说明了一个由 C 语言编写的库也可以服务于其他编程语言。

静态库和动态库对比

Linux 系统存在两种类型库:

  • 静态库(也被称为归档库):在编译过程中的链接阶段,静态库会被编译进程序(例如 C 或 Rust)中。每个客户程序都有属于自己的一份库的拷贝。静态库有一个显而易见的缺点 —— 当库需要进行一定改动时(例如修复一个 bug),静态库必须重新链接一次。接下来要介绍的动态库避免了这一缺点。
  • 动态库(也被称为共享库):动态库首先会在程序编译中的链接阶段被标记,但是客户程序和库代码在运行之前仍然没有联系,且库代码不会进入到客户程序中。系统的动态加载器会把一个共享库和正在运行的客户程序进行连接,无论该客户程序是由静态编译语言(如 C)编写,还是由动态解释语言(如 Python)编写。因此,动态库不需要麻烦客户程序便可以进行更新。最后,多个客户程序可以共享同一个动态库的单一副本。

通常来说,动态库优于静态库,尽管其复杂性较高而性能较低。下面是两种类型的库如何创建和发布:

  1. 库的源代码会被编译成一个或多个目标模块,目标模块是二进制文件,可以被包含在库中并且链接到可执行的二进制中。
  2. 目标模块会会被打包成一个文件。对于静态库,标准的文件拓展名是 .a 意为“ 归档 archive ”;对于动态库,标准的文件拓展名是 .so 意为“ 共享目标 shared object ”。对于这两个相同功能的示例库,分别发布为 libprimes.a (静态库)和 libshprimes.so (动态库)。两种库的文件名都使用前缀 lib 进行标识。
  3. 库文件被复制到标准目录下,使得客户程序可以轻松地访问到库。无论是静态库还是动态库,典型的位置是 /usr/lib 或者 /usr/local/lib,当然其他位置也是可以的。

构建和发布每种库的具体步骤会在下面详细介绍。首先我将介绍两种库里涉及到的 C 函数。

示例库函数

这两个示例库都是由五个相同的 C 函数构建而成的,其中四个函数可供客户程序使用。第五个函数是其他四个函数的一个工具函数,它显示了 C 语言怎么隐藏信息。每个函数的源代码都很短,可以将这些函数放在单个源文件中,尽管也可以放在多个源文件中(如四个公布的函数都有一个文件)。

这些库函数是基本的处理函数,以多种方式来处理质数。所有的函数接收无符号(即非负)整数值作为参数:

  • is_prime 函数测试其单个参数是否为质数。
  • are_coprimes 函数检查了其两个参数的 最大公约数 greatest common divisor (gcd)是否为 1,即是否为互质数。
  • prime_factors:函数列出其参数的质因数。
  • glodbach:函数接收一个大于等于 4 的偶数,列出其可以分解为两个质数的和。它也许存在多个符合条件的数对。该函数是以 18 世纪数学家 克里斯蒂安·哥德巴赫 Christian Goldbach 命名的,他的猜想是任意一个大于 2 的偶数可以分解为两个质数之和,这依旧是数论里最古老的未被解决的问题。

工具函数 gcd 留在已部署的库文件中,但是在没有包含这个函数的文件无法访问此函数。因此,一个使用库的客户程序无法调用 gcd 函数。仔细观察 C 函数可以明白这一点。

更多关于 C 函数的内容

每个在 C 语言中的函数都有一个存储类,它决定了函数的范围。对于函数,有两种选择。

  • 函数默认的存储类是 extern,它给了函数一个全局域。一个客户程序可以调用在示例库中用 extern 修饰的任意函数。下面是一个带有显式 extern 声明的 are_coprimes 函数定义:
extern unsigned are_coprimes(unsigned n1, unsigned n2) {
  ...
}
  • 存储类 static 将一个函数的的范围限制到函数被定义的文件中。在示例库中,工具函数 gcd 是静态的(static):
static unsigned gcd(unsigned n1, unsigned n2) {
  ...
}

只有在 primes.c 文件中的函数可以调用 gcd,而只有 are_coprimes 函数会调用它。当静态库和动态库被构建和发布后,其他的程序可以调用外部的(extern)函数,如 are_coprimes ,但是不可以调用静态(static)函数 gcd。静态(static)存储类通过将函数范围限制在其他库函数内,进而实现了对库的客户程序隐藏 gcd 函数。

primes.c 文件中除了 gcd 函数外,其他函数并没有指明存储类,默认将会设置为外部的(extern)。然而,在库中显式注明 extern 更加常见。

C 语言区分了函数的 定义 definition 声明 declaration ,这对库来说很重要。接下来让我们开始了解定义。C 语言仅允许命名函数不允许匿名函数,并且每个函数需要定义以下内容:

  • 一个唯一的名字。一个程序不允许存在两个同名的函数。
  • 一个可以为空的参数列表。参数需要指明类型。
  • 一个返回值类型(例如:int 代表 32 位有符号整数),当没有返回值时设置为空类型(void)。
  • 用一对花括号包围起来的函数主体部分。在一个特制的示例中,函数主体部分可以为空。

程序中的每个函数必须要被定义一次。

下面是库函数 are_coprimes 的完整定义:

extern unsigned are_coprimes(unsigned n1, unsigned n2) { /* 定义 */
  return 1 == gcd(n1, n2); /* 最大公约数是否为 1? */
}

函数返回一个布尔值(0 代表假,1 代表真),取决于两个整数参数值的最大公约数是否为 1。工具函数 gcd 计算两个整数参数 n1n2 的最大公约数。

函数声明不同于定义,其不需要主体部分:

extern unsigned are_coprimes(unsigned n1, unsigned n2); /* 声明 */

声明在参数列表后用一个分号代表结束,它没有被花括号包围起来的主体部分。程序中的函数可以被多次声明。

为什么需要声明?在 C 语言中,一个被调用的函数必须对其调用者可见。有多种方式可以提供这样的可见性,具体依赖于编译器如何实现。一个必然可行的方式就是当它们二者位于同一个文件中时,将被调用的函数定义在在它的调用者之前。

void f() {...}     /* f 定义在其被调用前 */
void g() { f(); }  /* ok */

当函数 f 被在调用前声明,此时函数 f 的定义可以移动到函数 g 的下方。

void f();         /* 声明使得函数 f 对调用者可见 */
void g() { f(); } /* ok */
void f() {...}    /* 相较于前一种方式,此方式显得更简洁 */

但是当如果一个被调用的函数和调用它的函数不在同一个文件中时呢?因为前文提到一个函数在一个程序中需要被定义一次,那么如何使得让一个文件中被定义的函数在另一个文件中可见?

这个问题会影响库,无论是静态库还是动态库。例如在这两个质数库中函数被定义在源文件 primes.c 中,每个库中都有该函数的二进制副本,但是这些定义的函数必须要对使用库的 C 程序可见,该 C 程序有其自身的源文件。

函数声明可以帮助提供跨文件的可见性。对于上述的“质数”例子,它有一个名为 primes.h 的头文件,其声明了四个函数使得它们对使用库的 C 程序可见。

/** 头文件 primes.h:函数声明 **/
extern unsigned is_prime(unsigned);
extern void prime_factors(unsigned);
extern unsigned are_coprimes(unsigned, unsigned);
extern void goldbach(unsigned);

这些声明通过为每个函数指定其调用语法来作为接口。

为了客户程序的便利性,头文件 primes.h 应该存储在 C 编译器查找路径下的目录中。典型的位置有 /usr/include/usr/local/include。一个 C 语言客户程序应使用 #include 包含这个头文件,并尽可能将这条语句其程序源代码的首部(头文件将会被导入另一个源文件的“头”部)。C 语言头文件可以被导入其他语言(如 Rust 语言)中的 bindgen,使其它语言的客户程序可以访问 C 语言的库。

总之,一个库函数只可以被定义一次,但可以在任何需要它的地方进行声明,任一使用 C 语言库的程序都需要该声明。头文件可以包含函数声明,但不能包含函数定义。如果头文件包含了函数定义,那么该文件可能会在一个 C 语言程序中被多次包含,从而破坏了一个函数在 C 语言程序中必须被精确定义一次的规则。

库的源代码

下面是两个库的源代码。这部分代码、头文件、以及两个示例客户程序都可以在 我的网页 上找到。

#include <stdio.h>
#include <math.h>

extern unsigned is_prime(unsigned n) {
  if (n <= 3) return n > 1;                   /* 2 和 3 是质数 */
  if (0 == (n % 2) || 0 == (n % 3)) return 0; /* 2 和 3 的倍数不会是质数 */

  /* 检查 n 是否是其他 < n 的值的倍数 */
  unsigned i;
  for (i = 5; (i * i) <= n; i += 6)
    if (0 == (n % i) || 0 == (n % (i + 2))) return 0; /* 不是质数 */

  return 1; /* 一个不是 2 和 3 的质数 */
}

extern void prime_factors(unsigned n) {
  /* 在数字 n 的质因数分解中列出所有 2 */
  while (0 == (n % 2)) {  
    printf("%i ", 2);
    n /= 2;
  }

  /* 数字 2 已经处理完成,下面处理奇数 */
  unsigned i;
  for (i = 3; i <= sqrt(n); i += 2) {
    while (0 == (n % i)) {
      printf("%i ", i);
      n /= i;
    }
  }

  /* 还有其他质因数?*/
  if (n > 2) printf("%i", n);
}

/* 工具函数:计算最大公约数 */
static unsigned gcd(unsigned n1, unsigned n2) {
  while (n1 != 0) {
    unsigned n3 = n1;
    n1 = n2 % n1;
    n2 = n3;
  }
  return n2;
}

extern unsigned are_coprimes(unsigned n1, unsigned n2) {
  return 1 == gcd(n1, n2);
}

extern void goldbach(unsigned n) {
  /* 输入错误 */
  if ((n <= 2) || ((n & 0x01) > 0)) {
    printf("Number must be > 2 and even: %i is not.\n", n);
    return;
  }

  /* 两个简单的例子:4 和 6 */
  if ((4 == n) || (6 == n)) {
    printf("%i = %i + %i\n", n, n / 2, n / 2);
    return;
  }
 
  /* 当 n > 8 时,存在多种可能性 */
  unsigned i;
  for (i = 3; i < (n / 2); i++) {
    if (is_prime(i) && is_prime(n - i)) {
      printf("%i = %i + %i\n", n, i, n - i);
      /* 如果只需要一对,那么用 break 语句替换这句 */
    }
  }
}

库函数

这些函数可以被库利用。两个库可以从相同的源代码中获得,同时头文件 primes.h 是两个库的 C 语言接口。

构建库

静态库和动态库在构建和发布的步骤上有一些细节的不同。静态库需要三个步骤,而动态库需要增加两个步骤即一共五个步骤。额外的步骤表明了动态库的动态方法具有更多的灵活性。让我们先从静态库开始。

库的源文件 primes.c 被编译成一个目标模块。下面是命令,百分号 % 代表系统提示符,两个井字符 # 是我的注释。

% gcc -c primes.c ## 步骤1(静态)

这一步生成目标模块是二进制文件 primes.o-c 标志意味着只编译。

下一步是使用 Linux 的 ar 命令将目标对象归档。

% ar -cvq libprimes.a primes.o ## 步骤2(静态)

-cvq 三个标识分别是“创建”、“详细的”、“快速添加”(以防新文件没有添加到归档中)的简称。回忆一下,前文提到过前缀 lib 是必须的,而库名是任意的。当然,库的文件名必须是唯一的,以避免冲突。

归档已经准备好要被发布:

% sudo cp libprimes.a /usr/local/lib ## 步骤3(静态)

现在静态库对接下来的客户程序是可见的,示例在后面。(包含 sudo 可以确保有访问权限将文件复制进 /usr/local/lib 目录中)

动态库还需要一个或多个对象模块进行打包:

% gcc primes.c -c -fpic ## 步骤1(动态)

增加的选项 -fpic 指示编译器生成与位置无关的代码,这意味着不需要将该二进制模块加载到一个固定的内存位置。在一个拥有多个动态库的系统中这种灵活性是至关重要的。生成的对象模块会略大于静态库生成的对象模块。

下面是从对象模块创建单个库文件的命令:

% gcc -shared -Wl,-soname,libshprimes.so -o libshprimes.so.1 primes.o ## 步骤2(动态)

选项 -shared 表明了该库是一个共享的(动态的)而不是静态的。-Wl 选项引入了一系列编译器选项,第一个便是设置动态库的 -soname,这是必须设置的。soname 首先指定了库的逻辑名字(libshprimes.so),接下来的 -o 选项指明了库的物理文件名字(libshprimes.so.1)。这样做的目的是为了保持逻辑名不变的同时允许物理名随着新版本而发生变化。在本例中,在物理文件名 libshprimes.so.1 中最后的 1 代表是第一个库的版本。尽管逻辑文件名和物理文件名可以是相同的,但是最佳做法是将它们命名为不同的名字。一个客户程序将会通过逻辑名(本例中为 libshprimes.so)来访问库,稍后我会进一步解释。

接下来的一步是通过复制共享库到合适的目录下使得客户程序容易访问,例如 /usr/local/lib 目录:

% sudo cp libshprimes.so.1 /usr/local/lib ## 步骤3(动态)

现在在共享库的逻辑名(libshprimes.so)和它的物理文件名(/usr/local/lib/libshprimes.so.1)之间设置一个符号链接。最简单的方式是将 /usr/local/lib 作为工作目录,在该目录下输入命令:

% sudo ln --symbolic libshprimes.so.1 libshprimes.so ## 步骤4(动态)

逻辑名 libshprimes.so 不应该改变,但是符号链接的目标(libshrimes.so.1)可以根据需要进行更新,新的库实现可以是修复了 bug,提高性能等。

最后一步(一个预防措施)是调用 ldconfig 工具来配置系统的动态加载器。这个配置保证了加载器能够找到新发布的库。

% sudo ldconfig ## 步骤5(动态)

到现在,动态库已为包括下面的两个在内的示例客户程序准备就绪了。

一个使用库的 C 程序

这个示例 C 程序是一个测试程序,它的源代码以两条 #include 指令开始:

#include <stdio.h>  /* 标准输入/输出函数 */
#include <primes.h> /* 我的库函数 */

文件名两边的尖括号表示可以在编译器的搜索路径中找到这些头文件(对于 primes.h 文件来说在 /usr/local/inlcude 目录下)。如果不包含 #include,编译器会抱怨缺少 is_primeprime_factors 等函数的声明,它们在两个库中都有发布。顺便提一句,测试程序的源代码不需要更改即可测试两个库中的每一个库。

相比之下,库的源文件(primes.c)使用 #include 指令打开以下头文件:

#include <stdio.h>
#include <math.h>

math.h 头文件是必须的,因为库函数 prime_factors 会调用数学函数 sqrt,其在标准库 libm.so 中。

作为参考,这是测试库程序的源代码:

#include <stdio.h>
#include <primes.h>

int main() {
  /* 是质数 */
  printf("\nis_prime\n");
  unsigned i, count = 0, n = 1000;
  for (i = 1; i <= n; i++) {
    if (is_prime(i)) {
      count++;
      if (1 == (i % 100)) printf("Sample prime ending in 1: %i\n", i);
    }
  }
  printf("%i primes in range of 1 to a thousand.\n", count);

  /* prime_factors */
  printf("\nprime_factors\n");
  printf("prime factors of 12: ");
  prime_factors(12);
  printf("\n");
 
  printf("prime factors of 13: ");
  prime_factors(13);
  printf("\n");
 
  printf("prime factors of 876,512,779: ");
  prime_factors(876512779);
  printf("\n");

  /* 是合数 */
  printf("\nare_coprime\n");
  printf("Are %i and %i coprime? %s\n",
         21, 22, are_coprimes(21, 22) ? "yes" : "no");
  printf("Are %i and %i coprime? %s\n",
         21, 24, are_coprimes(21, 24) ? "yes" : "no");

  /* 哥德巴赫 */
  printf("\ngoldbach\n");
  goldbach(11);    /* error */
  goldbach(4);     /* small one */
  goldbach(6);     /* another */
  for (i = 100; i <= 150; i += 2) goldbach(i);

  return 0;
}

测试程序

在编译 tester.c 文件到可执行文件时,难处理的部分时链接选项的顺序。回想前文中提到两个示例库都是用 lib 作为前缀开始,并且每一个都有一个常规的拓展后缀:.a 代表静态库 libprimes.a.so 代表动态库 libshprimes.so。在链接规范中,前缀 lib 和拓展名被忽略了。链接标志以 -l (小写 L)开始,并且一条编译命令可能包含多个链接标志。下面是一个完整的测试程序的编译指令,使用动态库作为示例:

% gcc -o tester tester.c -lshprimes -lm

第一个链接标志指定了库 libshprimes.so,第二个链接标志指定了标准数学库 libm.so

链接器是懒惰的,这意味着链接标志的顺序是需要考虑的。例如,调整上述实例中的链接顺序将会产生一个编译时错误:

% gcc -o tester tester.c -lm -lshprimes ## 危险!

链接 libm.so 库的标志先出现,但是这个库中没有函数被测试程序显式调用;因此,链接器不会链接到 math.so 库。调用 sqrt 库函数仅发生在 libshprimes.so 库中包含的 prime_factors 函数。编译测试程序返回的错误是:

primes.c: undefined reference to 'sqrt'

因此,链接标志的顺序应该是通知链接器需要 sqrt 函数:

% gcc -o tester tester.c -lshprimes -lm ## 首先链接 -lshprimes

链接器在 libshprimes.so 库中发现了对库函数 sqrt 的调用,所以接下来对数学库 libm.so做了合适的链接。链接还有一个更复杂的选项,它支持链接的标志顺序。然而在本例中,最简单的方式就是恰当地排列链接标志。

下面是运行测试程序的部分输出结果:

is_prime
Sample prime ending in 1: 101
Sample prime ending in 1: 401
...
168 primes in range of 1 to a thousand.

prime_factors
prime factors of 12: 2 2 3
prime factors of 13: 13
prime factors of 876,512,779: 211 4154089

are_coprime
Are 21 and 22 coprime? yes
Are 21 and 24 coprime? no

goldbach
Number must be &gt; 2 and even: 11 is not.
4 = 2 + 2
6 = 3 + 3
...
32 =  3 + 29
32 = 13 + 19
...
100 =  3 + 97
100 = 11 + 89
...

对于 goldbach 函数,即使一个相当小的偶数值(例如 18)也许存在多个一对质数之和的组合(在这种情况下,5+13 和 7+11)。因此这种多个质数对是使得尝试证明哥德巴赫猜想变得复杂的因素之一。

封装使用库的 Python 程序

与 C 不同,Python 不是一个静态编译语言,这意味着 Python 客户示例程序必须访问动态版本而非静态版本的 primes 库。为了能这样做,Python 中有众多的支持 外部语言接口 foreign function interface (FFI)的模块(标准的或第三方的),它们允许用一种语言编写的程序来调用另一种语言编写的函数。Python 中的 ctypes 是一个标准的、相对简单的允许 Python 代码调用 C 函数的 FFI。

任何 FFI 都面临挑战,因为对接的语言不大可能会具有完全相同的数据类型。例如:primes 库使用 C 语言类型 unsigned int,而 Python 并不具有这种类型;因此 ctypes FFI 将 C 语言中的 unsigned int 类型映射为 Python 中的 int 类型。在 primes 库中发布的四个 extern C 函数中,有两个在具有显式 ctypes 配置的 Python 中会表现得更好。

C 函数 prime_factorsgoldbach 返回 void 而不是返回一个具体类型,但是 ctypes 默认会将 C 语言中的 void 替换为 Python 语言中的 int。当从 Python 代码中调用时,这两个 C 函数会从栈中返回一个随机整数值(因此,该值无任何意义)。然而,可以对 ctypes 进行配置,让这些函数返回 None (Python 中为 null 类型)。下面是对 prime_factors 函数的配置:

primes.prime_factors.restype = None

可以用类似的语句处理 goldbach 函数。

下面的交互示例(在 Python3 中)展示了在 Python 客户程序和 primes 库之间的接口是简单明了的。

>>> from ctypes import cdll

>>> primes = cdll.LoadLibrary("libshprimes.so") ## 逻辑名

>>> primes.is_prime(13)
1
>>> primes.is_prime(12)
0

>>> primes.are_coprimes(8, 24)
0
>>> primes.are_coprimes(8, 25)
1

>>> primes.prime_factors.restype = None
>>> primes.goldbach.restype = None

>>> primes.prime_factors(72)
2 2 2 3 3

>>> primes.goldbach(32)
32 = 3 + 29
32 = 13 + 19

primes 库中的函数只使用一个简单数据类型:unsigned int。如果这个 C 语言库使用复杂的类型如结构体,如果库函数传递和返回指向结构体的指针,那么比 ctypes 更强大的 FFI 更适合作为一个在 Python 语言和 C 语言之间的平滑接口。尽管如此,ctypes 示例展示了一个 Python 客户程序可以使用 C 语言编写的库。值得注意的是,用作科学计算的流行的 Numpy 库是用 C 语言编写的,然后在高级 Python API 中公开。

简单的 primes 库和高级的 Numpy 库强调了 C 语言仍然是编程语言中的通用语言。几乎每一个语言都可以与 C 语言交互,同时通过 C 语言也可以和任何其他语言交互。Python 很容易和 C 语言交互,作为另外一个例子,当 Panama 项目 成为 Java Native Interface(JNI)一个替代品后,Java 语言和 C 语言交互也会变的很容易。


via: https://opensource.com/article/21/2/linux-software-libraries

作者:Marty Kalin 选题:lujun9972 译者:萌新阿岩 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

探索一些未被充分利用但仍然有用的 Python 特性。

 title=

这是 Python 3.x 首发特性系列文章的第一篇。Python 3.0 于 2008 年首次发布,尽管它已经发布了一段时间,但它引入的许多特性都没有被充分利用,而且相当酷。这里有三个你应该知道的。

仅限关键字参数

Python 3.0 首次引入了仅限关键字参数参数的概念。在这之前,不可能指定一个只通过关键字传递某些参数的 API。这在有许多参数,其中一些参数可能是可选的函数中很有用。

请看一个特意设计的例子:

def show_arguments(base, extended=None, improved=None, augmented=None):
    print("base is", base)
    if extended is not None:
        print("extended is", extended)
    if improved is not None:
        print("improved is", improved)
    if augmented is not None:
        print("augmented is", augmented)

当阅读调用该函数的代码时,有时很难理解发生了什么:

show_arguments("hello", "extra")

    base is hello
    extended is extra

show_arguments("hello", None, "extra")

    base is hello
    improved is extra

虽然可以用关键字参数来调用这个函数,但这明显不是最好的方法。相反,你可以将这些参数标记为仅限关键字:

def show_arguments(base, *, extended=None, improved=None, augmented=None):
    print("base is", base)
    if extended is not None:
        print("extended is", extended)
    if improved is not None:
        print("improved is", improved)
    if augmented is not None:
        print("augmented is", augmented)

现在,你不能用位置参数传入额外的参数:

show_arguments("hello", "extra")
    ---------------------------------------------------------------------------

    TypeError                                 Traceback (most recent call last)

    <ipython-input-7-6000400c4441> in <module>
    ----> 1 show_arguments("hello", "extra")
   

    TypeError: show_arguments() takes 1 positional argument but 2 were given

对该函数的有效调用更容易预测:

show_arguments("hello", improved="extra")
    base is hello
    improved is extra

nonlocal

有时,函数式编程的人根据编写累加器的难易程度来判断一种语言。累加器是一个函数,当它被调用时,返回目前为止发给它的所有参数的总和。

在 3.0 之前,Python 的标准答案是:

class _Accumulator:
    def __init__(self):
        self._so_far = 0
    def __call__(self, arg):
        self._so_far += arg
        return self._so_far

def make_accumulator():
    return _Accumulator()

虽然我承认有些啰嗦,但这确实有效:

acc = make_accumulator()
print("1", acc(1))
print("5", acc(5))
print("3", acc(3))

这样做的输出结果将是:

1 1
5 6
3 9

在 Python 3.x 中,nonlocal 关键字可以用少得多的代码实现同样的行为。

def make_accumulator():
    so_far = 0
    def accumulate(arg):
        nonlocal so_far
        so_far += arg
        return so_far
    return accumulate

虽然累加器是人为的例子,但使用 nonlocal 关键字使内部函数拥有具有状态的的能力是一个强大的工具。

扩展析构

想象一下,你有一个 CSV 文件,每一行由几个元素组成:

  • 第一个元素是年份
  • 第二个元素是月
  • 其他元素是该月发表的全部文章数,每天一个条目

请注意,最后一个元素是 文章总数,而不是 每天发表的文章。例如,一行的开头可以是:

2021,1,5,8,10

这意味着在 2021 年 1 月,第一天发表了 5 篇文章。第二天,又发表了三篇文章,使总数达到 8 篇。第三天,又发表了两篇文章。

一个月可以有 28 天、30 天或 31 天。提取月份、日期和文章总数有多难?

在 3.0 之前的 Python 版本中,你可能会这样写:

year, month, total = row[0], row[1], row[-1]

这是正确的,但它掩盖了格式。使用扩展析构,同样可以这样表达:

year, month, *rest, total = row

这意味着如果该格式改为前缀了一个描述,你可以把代码改成:

_, year, month, *rest, total = row

而不需要在每个索引中添加 1

接下来是什么?

Python 3.0 和它的后期版本已经推出了 12 年多,但是它的一些功能还没有被充分利用。在本系列的下一篇文章中,我将会写另外三个。


via: https://opensource.com/article/21/5/python-30-features

作者:Moshe Zadka 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出