观察你的代码在其他解释器下运行的表现或许是一项有趣的尝试。作为最受欢迎的编程语言之一，Python 需要一个解释器来执行其代码所定义的命令。与其他可直接编译成机器代码的语言不同，Python 代码需要解释器读取它并把它转译给进行相关操作的 CPU。那么，哪些解释器有哪些呢？本文将对其中几种进行介绍。解释器简介提到 Python 解释器，我们通常会想到 /usr/bin/python 这个二进制文件。它使你能够执行 .py 文件。然而，解释操作仅仅是其中一环。在 Python 代码真正被 CPU 执行之前，都需要经过以下四个步骤：词法分析 - 将人类编写的源代码转换为一序列逻辑实体，被称为 词法标记 lexical token 。解析 - 解析器会检查词法标记的语法和语义规则生成 抽象语法树 abstract syntax tree （AST）。编译 - 编译器会根据 AST 创建 Python 字节码，这些字节码由非常基础的，和平台无关的指令组成。解释 - 解释器处理字节码并执行特定的操作。如你所见，在任何实质性的操作发生之前，我们需要走过这些步骤。这也解释了深入研究不同解释器的重要性。1、CPython作为 Python 的参考实现，CPython 默认地被许多系统所采用。如其名称所示，CPython 是用 C 语言编写的。这也意味着，我们可以 以 C 语言编写扩展，从而让 Python 打通到广泛使用的 C 语言库代码。CPython 广泛应用于各种平台，包括 ARM 和 RISC。然而，作为 Python 的参考实现，CPython 更注重精细的优化，而非运行速度。2、PystonPyston 是一个从 CPython 解释器衍生出的分支，其中实现了性能优化。该项目定位自己为标准 CPython 解释器在处理大型、真实世界应用时的替代品，并有可能加速高达 30%。由于缺乏兼容的二进制包，Pyston 在下载过程中需要重新编译。3、PyPy采用了 RPython 编写的 PyPy 是一个专为 Python 配备的 即时（JIT） 编译器，RPython 是 Python 的一个静态类型的子集。不同于 CPython 解释器，PyPy 对源代码进行编译，生成 CPU 可直接执行的机器码。PyPy 是 Python 开发者的实验室，在这里他们能更容易地测试新特性。相较于 CPython，PyPy 的执行速度更快。由于 JIT 编译器的特性，长时间运行的应用更能从缓存中受益。PyPy 可以被视为 CPython 的有效替代。虽然其中存在一些缺点，大部分的 C 扩展模块在 PyPy 中也得到支持，但运行速度会相对慢一些。PyPy 扩展模块使用 Python（而不是 C）编写，这使 JIT 编译器能够对其进行优化。只要你的应用程序不依赖于不兼容的模块，PyPy 就是替换 CPython 的理想选择。你可以在项目官网找到一个专门的页面，详细描述 PyPy 与 CPython 的不同之处：PyPy 与 CPython 的差异4、RustPython顾名思义，RustPython 是一个由 Rust 编写的 Python 解释器。尽管 Rust 如今还是一个相对年轻的编程语言，但因其优良特性已逐步受到开发者的推崇，甚至被视为 C 和 C++ 的可能接班人。默认情况下，RustPython 的行为与 CPython 的解释器类似，但它也可以选择启用 JIT 编译器。值得一提的是，Rust 工具链能直接编译为 WebAssembly ，进而允许在浏览器中全面运行解释器。你可以在 这里 看到它的在线演示。5、Stackless PythonStackless Python 自称是 Python 编程语言的增强版本。该项目基本上是 CPython 解释器衍生的一个项目，其为该语言添加了微线程、通道和调度器。微线程可以帮助你将代码组织成可以并行运行的 “ 小任务 tasklet ”。这与采用 greenlet 模块的绿色线程模型相似。通道可以用作 “小任务” 之间的双向通信。Stackless Python 的一个知名用户是大型多人在线角色扮演游戏 Eve Online。6、Micro Python如果你的目标平台是微控制器，那么 MicroPython 将是你的首选。它是一种极简的实现，只需要 16kB 的内存和 256kB 的存储空间。由于其主要面向的是嵌入式环境，MicroPython 的标准库只包含 CPython 丰富的 STL 的一部分。对于开发和测试，或者作为轻量级替代品，MicroPython 也可以在普通的 x86 和 x64 系统上运行。MicroPython 支持 Linux、Windows，以及多种微控制器。性能就其设计而言，Python 本质上是一种运行速度不够快的语言。根据任务性质的不同，各种解释器间存在明显的性能差异。要想弄清楚哪种解释器最适合特定任务，可以参考 pybenchmarks.org。与使用解释器相比，另一种选择是直接将 Python 二进制代码编译成机器码，例如，Nuitka 就是能够完成这种工作的项目之一，它可以将 Python 代码编译成 C 代码，然后将 C 代码通过常规的 C 编译器编译成机器码。Python 编译器的主题范围广泛，值得一篇独立的文章来详述。总结Python 是构建快速原型和自动化任务的优秀语言，同时它又易于学习，对初学者友好。如果你平时维持使用 CPython，那么尝试看看你的代码在另一解释器上运行会是什么样子也许会很有趣。如果你是 Fedora 用户，你可以轻松地测试几种其他解释器，因为其包管理器已经提供了需要的二进制文件。你可以在 fedora.developer.org 上查找更多信息。（题图：MJ/9b24f27b-bd2b-4916-9f33-bcfb9e2b1d33）via: https://opensource.com/article/22/9/python-interpreters-2022作者：Stephan Avenwedde 选题：lkxed 译者：ChatGPT 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Doxygen 是一款广泛使用的开源文档生成工具，它通过代码注释来生成文档。在试着熟悉别人的代码时，你总希望他们留下的代码注释能对你理解代码有所帮助。同理，无论为了自己还是其他人，编写代码时写注释是好习惯。所有编程语言都有专门的注释语法，注释可以是一个单词、一行文字、甚至是一整段话。编译器或解释器处理源代码时会忽略注释。注释不能完全取代文档，但是有方法可以使用注释来生成文档。Doxygen 是一个开源的文档生成工具，它能够根据代码注释生成 HTML 或 LaTeX 格式的文档。Doxygen 让你在不用额外操作的情况下创建代码结构概览。尽管 Doxygen 主要是用来给 C++ 生成文档的，它对其它语言同样适用，比如 C、Objective-C、 C#、 PHP、Java 和 Python 等。要使用 Doxygen，你只需要在源代码中使用 Doxygen 能够识别的语法来写注释。Doxygen 会扫描源码文件，然后根据这些特殊注释生成 HTML 或 LaTeX 文档。下面的示例项目会演示如何使用 Doxygen 注释，以及文档是如通过注释生成出来的。示例代码可从 GitHub 上获得，本文中也将引用 Doxygen 手册及文档 的相关章节。在 Linux 上安装 Doxygen在 Fedora 上可以通过软件包的形式安装 Doxygen。打开终端运行命令：sudo dnf install doxygen 在基于 Debian 的操作系统上，可以通过以下命令来安装：sudo apt-get install doxygen 使用安装完 Doxygen 后，你需要在项目中按 Doxygen 可以识别的格式来注释代码，还要提供一个 Doxyfile 配置文件来控制 Doxygen 的一些行为。注意：如果你用的是 GitHub 上的示例项目，你可以忽略下面一步。如果 Doxyfile 文件不存在，你可以用 Doxygen 生成一个标准 Doxyfile 模板文件。切换到项目根目录下，运行：doxygen -g 参数 -g 表示 生成 generate 。现在应该会出现一个名为 Doxyfile 的新文件。通过命令调用 Doxygen：doxygen 现在应该能会有两个新文件夹：html/latex/默认情况下，Doxygen 会同时输出 LaTeX 和 HTML 格式的文档。本文主要关注 HTML 文档。你可以在 Doxygen 官方文档的入门小节中找到关于 LaTeX 格式输出的更多信息。双击 html/index.html 打开 HTML 文件。用空的配置文件生成的文档如下图：现在我们试着修改 Doxyfile 文件，并在源代码中添加特殊注释。Doxyfile 文件在 Doxyfile 文件中可以定义大量的可调选项，本文通过介绍示例项目的 Doxyfile 文件我只能覆盖其中很小的子集。第 35 行：项目名称你可以在这里指定项目名称，它最终会显示在 页眉 header 和浏览器标签上。# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by # double-quotes, unless you are using Doxywizard) that should identify the # project for which the documentation is generated. This name is used in the # title of most generated pages and in a few other places. # The default value is: My Project. PROJECT_NAME = "My Project" 第 47 行：项目简介项目简介会以略小的字号显示在页眉上。# Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a # quick idea about the purpose of the project. Keep the description short. PROJECT_BRIEF = "An example of using Doxygen in C++" 第 926 行：包含子目录允许 Doxygen 查找源代码和文档文件时递归遍历子目录。# The RECURSIVE tag can be used to specify whether or not subdirectories should # be searched for input files as well. # The default value is: NO. RECURSIVE = YES 第 1769 行：禁用 LaTeX 输出如果你只想生成 HTML 文档，可以通过这个开关禁用 LaTeX 输出。# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. # The default value is: YES. GENERATE_LATEX = NO 修改完成后，你可以再次运行 Doxygen 来检验修改是否生效了。可以在调用 Doxygen 时使用 -x 选项来查看 Doxyfile 文件的变更项：通过调用 diff 命令，Doxygen 仅显示当前 Doxyfile 文件和模板文件的差异。特殊注释Doxygen 通过扫描源代码文件中的特殊注释和关键字来生成 HTML 文档。示例项目中的 ByteStream 类的头文件可以很好地解释特殊注释的用法。下面用构造函数和析构函数作为示例：/*! @brief Constructor which takes an external buffer to operate on * * The specified buffer already exist. * Memory and size can be accessed by buffer() and size(). * * @param[in] pBuf Pointer to existing buffer * @param[in] size Size of the existing buffer */ ByteStream(char* pBuf, size_t size) noexcept; 特殊注释块有不同的格式风格。我倾向于使用 /*! 开头（Qt 风格），每行前添加 *，以 */ 结束注释块。你可以参考 Doxygen 手册的文档化代码小节，以大致了解不同的风格选项。Doxygen 注释分两个部分：简要描述和详细描述。它们都是可选的。在上面的例子中的注释块是对紧跟其后的构造函数声明的描述。在 @brief 之后的文本会显示在类概览小节中：在空行（空行是段落分隔符）之后是构造函数的实际文档。用 @param[in/out] 关键字标注传递给构造函数的参数，Doxygen 基于此生成参数列表：值得注意的是 Doxygen 为 buffer() 和 size() 方法自动生成了链接。相反，Doxygen 忽略了析构函数前的注释，因为它并没有使用特殊注释：// Destructor ~ByteStream(); 现在你已经看到 Doxygen 的绝大部分功能了。通过使用一种稍微改良的注释格式，让 Doxygen 能够识别它们。通过使用一些关键字，你甚至可以进一步控制格式化。在下一节中，我会进一步介绍 Doxygen 的其它特性。其它特性现在几乎所有的工作都可以通过对源代码注释的方式完成。通过一些微调，你可以轻松地优化 Doxygen 的输出。Markdown 格式为了进阶的格式化，Doxygen 支持 Markdown 和 HTML 命令。Markdown 速查表可以在 这里 下载到。项目主页除了自定义页眉之外，html/index.html 几乎没有其它内容了。你可以通过使用关键字向其中添加一些有意义的内容。因为主页通常不是针对某个源代码文件的，你可以将要显示在主页的内容放到项目根目录下的一个单独文件中。示例项目中就是这样做的，其输出效果如下：自动链接生成上面已将提到了，当你引用代码的其它部分时，Doxygen 会自动识别并生成相应链接。但要注意，这要求被引用部分也有文档才行。更多信息可以在官方文档的自动链接生成中找到。分组ByteStream 类 重载 overload 了的读写流操作符 （<< 和 >>）。在类的概览中可以发现操作符被分为读和写两组。分组是在 ByteStream 的头文件中定义的。分组的语法以标记 @{ 开始，以 }@ 结束。在标记范围中的内容都属于这个分组。在 ByteStream.h 中的实现如下：/** @name Writing * Operators for writing to the stream * @{ */ (...) /** @} * @name Reading * Operators for reading from the stream * @{ */ (...) /** @} */ 你可以在官方文档的分组中找到更多相关信息。LLVM 支持如果你用 Clang 构建项目的话，可以通过使用 -Wdocumentation 选项让 Clang 对特殊注释进行检查。想了解该特性的更多信息，可以参考 LLVM 用户手册和 Dmitri Gribenko 的展示报告，它们可以在 Clang 网站上找到。谁在用 DoxygenDoxygen 是在 1997 年首次发布的。尽管有些年头了，现在仍然有很多项目在使用 Doxygen。比如 NASA 的飞行软件框架 F Prime、图像处理库 OpenCV、包管理器 RPM。你还可以在其它领域发现 Doxygen 语法标记的身影，比如内容管理平台 Drupal 的文档标准中。注意：Doxygen 输出的 HTML 文档风格类似于九十年代网页。并且它也难以描绘元编程和模板编程架构。在这些情况下，你应该选择 Sphinx 而不是 Doxygen。（题图：MJ/4d354094-397e-4ac5-a80d-25b9c736ede5）via: https://opensource.com/article/22/5/document-source-code-doxygen-linux作者：Stephan Avenwedde 选题：lkxed 译者：toknow-gh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
连续集成 continuous integration （CI）是指代码变更会被自动构建和测试。以下是我为自己的 C++ 项目构建 CI 流水线的过程。本文介绍如何在 GitLab 上配置 CI 流水线。我在前面的文章中介绍了 基于 CMake 和 VSCodium 的构建系统 和 基于 GoogleTest 和 CTest 的单元测试。本文将在此基础上进一步配置 CI 流水线。我会先演示如何布设和运行 CI 流水线，然后再介绍如何配置它。CI 是指提交到代码仓库的代码变更会被自动构建和测试。在开源领域，GitLab 是一个流行的 CI 流水线平台。除了作为中心 Git 仓库外，GitLab 还提供 CI/CD 流水线、 问题跟踪 issue tracking 和 容器注册表 container registry 功能。相关术语在进入正题之前，我先介绍在本文和 GitLab 文档 中会遇到的常见术语。 持续交付 continuous delivery （CD）：自动化供应软件，以供随时交付 持续部署 continuous deployment （CD）：自动化软件发布 流水线 pipeline : CI/CD 的直接构件，它由阶段和作业构成 阶段 stage ：一组作业 作业 job ：某项需要执行的具体任务，比如编译、单元测试等 执行器 runner ：实际执行作业的服务布设 CI 流水线在下面的章节中，我将复用以前的 示例工程。点击 GitLab 仓库页面右上角的 复刻 Fork 按钮复刻代码仓库。设置执行器为了让你对整个流程有所了解，我们先从在本地安装执行器讲起。参照执行器服务 安装指南 安装好服务，然后注册执行器。1、选择 GitLab 项目页面左侧的 设置 Settings ，再选择 CI/CD。2、展开 执行器 Runners 区域，关闭 共享的执行器 Shared runners 选项（黄框处）。特别注意令牌和 URL（绿框处），下一步会用到它们。3、在终端中运行 gitlab-runner register，根据提示输入以下注册信息：GitLab 实例: https://gitlab.com/ （如上图）注册令牌：从执行器区域中获取 （如上图）描述：按需自由填写标签：可以不填执行环境：选 Shell如果有需要，你可以在 ~/.gitlab-runner/config.toml 中修改这些配置。4、用命令 gitlab-runner run 启动执行器。你可以在 GitLab 的项目设置界面执行器区域看到执行器的状态：运行流水线前面已经提过，流水线就是一组由执行器执行的作业。每个推送到 GitLab 的提交都会生成一个附加到该提交的流水线。如果多个提交被一起推送，那么只会为最后一个提交生成流水线。为了演示，我直接在 GitLab 在线编辑器中提交和推送修改。打开 README.md 文件，添加一行数据：现在提交修改。这里注意默认的行为是为提交新建一个分支，为了简便起见，我们择提交到主分支。提交后一会儿后，你就应该改能看到 GitLab 执行器执行的控制台中有输出消息：Checking for jobs... received job=1975932998 repo_url=<https://gitlab.com/hANSIc99/cpp\_testing\_sample.git> runner=Z7MyQsA6 Job succeeded duration_s=3.866619798 job=1975932998 project=32818130 runner=Z7MyQsA6 在 GitLab 项目概览界面左侧选择 CI/CD --> 管道 Pipelines ，查看最近执行的流水线：选中流水线可以在详情界面看到哪些作业失败了，并能查看各个作业的输出。当遇到非零返回值是就认为作业执行失败了。在下面的例子中我通过调用 exit 1 强制让作业执行失败：CI 配置阶段、流水线和作业的配置都在仓库根目录的 .gitlab-ci.yml 文件中。我建议使用 GitLab 内置的流水线编辑器，它会自动对配置进行检查。stages: - build - test build: stage: build script: - cmake -B build -S . - cmake --build build --target Producer artifacts: paths: - build/Producer RunGTest: stage: test script: - cmake -B build -S . - cmake --build build --target GeneratorTest - build/Generator/GeneratorTest RunCTest: stage: test script: - cmake -B build -S . - cd build - ctest --output-on-failure -j6 文件中定义了两个阶段：build 和 test，以及三个作业：build、RunGTest 和 RunCTest。其中作业 build 属于一个同名的阶段，另外两个作业属于阶段 test。script 小节下的命令就是一般的 Shell 命令。你可以认为是将它们逐行输入到 Shell 中。我要特别提及 产物artifact 这个特性。在示例中我定义了二进制的 Producer 为作业 build 的产物。产物会被上传到 GitLab 服务器，并且可以从服务器的这个页面上被下载：默认情况下，后续阶段的作业会自动下载先前阶段作业生成的所有产物。你可以在 docs.gitlab.com 上查看 gitlab-ci.yml 参考指南。总结上面只是一个最基本的例子，让你对持续集成的一般原则有一个了解。再演示中我禁用了共享执行器，然而这才是 GitLab 的优势所在。你可以在一个干净的容器化的环境中构架、测试和部署程序。除了使用 GitLab 提供的免费执行器，你也可以用自己的容器作为执行器。当然还有更高阶的用法：用 Kubernetes 来协调调度执行者容器，让流水线适应大规模使用的使用场景。如需进一步了解，可以查看 about.gitlab.com。如果你使用的是 Fedora，需要注意的一点是目前 GitLab 执行者还不支持用 Podman 作为容器引擎。（LCTT 译注：Podman 是 Fedora 自带的容器引擎。）根据 议题 issue #27119，对 Podman 支持已将列上日程。（LCTT 译注：Podman 4.2 及以上版本增加了对于 GitLab 执行器的支持。）把重复性的操作描述成作业，并将作业合并成流水线和阶段，可以让你跟踪它们的质量而不增加额外工作。。特别是在大型社区项目中，适当配置的 CI 可以告诉你提交的代码是否对项目有改善，为你接受或拒绝合并请求提供依据。（题图：MJ/fb711c48-251a-4726-a41c-247370e5df25）via: https://opensource.com/article/22/2/setup-ci-pipeline-gitlab作者：Stephan Avenwedde 选题：lujun9972 译者：toknow-gh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
进行单元测试可以提高代码质量，并且它不会打断你的工作流。本文是 使用 CMake 和 VSCodium 设置一个构建系统 的后续文章。在上一篇文章中我介绍了基于 VSCodium 和 CMake 配置构建系统。本文我将介绍如何通过 GoogleTest 和 CTest 将单元测试集成到这个构建系统中。首先克隆 这个仓库，用 VSCodium 打开，切换到 devops_2 标签。你可以通过点击 main 分支符号（红框处），然后选择 devops_2 标签（黄框处）来进行切换：或者你可以通过命令行来切换：$ git checkout tags/devops_2 GoogleTestGoogleTest 是一个平台无关的开源 C++ 测试框架。单元测试是用来验证单个逻辑单元的行为的。尽管 GoogleTest 并不是专门用于单元测试的，我将用它对 Generator 库进行单元测试。在 GoogleTest 中，测试用例是通过断言宏来定义的。断言可能产生以下结果：成功: 测试通过。非致命失败: 测试失败，但测试继续。致命失败: 测试失败，且测试终止。致命断言和非致命断言通过不同的宏来区分：ASSERT_*： 致命断言，失败时终止。EXPECT_*： 非致命断言，失败时不终止。谷歌推荐使用 EXPECT_* 宏，因为当测试中包含多个的断言时，它允许继续执行。断言有两个参数：第一个参数是测试分组的名称，第二个参数是测试自己的名称。Generator 只定义了 generate(...) 函数，所以本文中所有的测试都属于同一个测试组：GeneratorTest。针对 generate(...) 函数的测试可以从 GeneratorTest.cpp 中找到。引用一致性检查generate(...) 函数有一个 std::stringstream 的引用作为输入参数，并且它也将这个引用作为返回值。第一个测试就是检查输入的引用和返回的引用是否一致。TEST(GeneratorTest, ReferenceCheck){ const int NumberOfElements = 10; std::stringstream buffer; EXPECT_EQ( std::addressof(buffer), std::addressof(Generator::generate(buffer, NumberOfElements)) ); } 在这个测试中我使用 std::addressof 来获取对象的地址，并用 EXPECT_EQ 来比较输入对象和返回对象是否是同一个。检查元素个数本测试检查作为输入的 std::stringstream 引用中的元素个数与输入参数中指定的个数是否相同。TEST(GeneratorTest, NumberOfElements){ const int NumberOfElements = 50; int nCalcNoElements = 0; std::stringstream buffer; Generator::generate(buffer, NumberOfElements); std::string s_no; while(std::getline(buffer, s_no, ' ')) { nCalcNoElements++; } EXPECT_EQ(nCalcNoElements, NumberOfElements); } 乱序重排本测试检查随机化引擎是否工作正常。如果连续调用两次 generate 函数，应该得到的是两个不同的结果。TEST(GeneratorTest, Shuffle){ const int NumberOfElements = 50; std::stringstream buffer_A; std::stringstream buffer_B; Generator::generate(buffer_A, NumberOfElements); Generator::generate(buffer_B, NumberOfElements); EXPECT_NE(buffer_A.str(), buffer_B.str()); } 求和校验与前面的测试相比，这是一个大体量的测试。它检查 1 到 n 的数值序列的和与乱序重排后的序列的和是否相等。 generate(...) 函数应该生成一个 1 到 n 的乱序的序列，这个序列的和应当是不变的。TEST(GeneratorTest, CheckSum){ const int NumberOfElements = 50; int nChecksum_in = 0; int nChecksum_out = 0; std::vector<int> vNumbersRef(NumberOfElements); // Input vector std::iota(vNumbersRef.begin(), vNumbersRef.end(), 1); // Populate vector // Calculate reference checksum for(const int n : vNumbersRef){ nChecksum_in += n; } std::stringstream buffer; Generator::generate(buffer, NumberOfElements); std::vector<int> vNumbersGen; // Output vector std::string s_no; // Read the buffer back back to the output vector while(std::getline(buffer, s_no, ' ')) { vNumbersGen.push_back(std::stoi(s_no)); } // Calculate output checksum for(const int n : vNumbersGen){ nChecksum_out += n; } EXPECT_EQ(nChecksum_in, nChecksum_out); } 你可以像对一般 C++ 程序一样调试这些测试。CTest除了嵌入到代码中的测试之外，CTest 提供了可执行程序的测试方式。简而言之就是通过给可执行程序传入特定的参数，然后用 正则表达式 对它的输出进行匹配检查。通过这种方式可以很容易检查程序对于不正确的命令行参数的反应。这些测试定义在顶层的 CMakeLists.txt 文件中。下面我详细介绍 3 个测试用例：参数正常如果输入参数是一个正整数，程序应该输出应该是一个数列：add_test(NAME RegularUsage COMMAND Producer 10) set_tests_properties(RegularUsage PROPERTIES PASS_REGULAR_EXPRESSION "^[0-9 ]+" ) 没有提供参数如果没有传入参数，程序应该立即退出并提示错误原因：add_test(NAME NoArg COMMAND Producer) set_tests_properties(NoArg PROPERTIES PASS_REGULAR_EXPRESSION "^Enter the number of elements as argument" ) 参数错误当传入的参数不是整数时，程序应该退出并报错。比如给 Producer 传入参数 ABC：add_test(NAME WrongArg COMMAND Producer ABC) set_tests_properties(WrongArg PROPERTIES PASS_REGULAR_EXPRESSION "^Error: Cannot parse" ) 执行测试可以使用 ctest -R Usage -VV 命令来执行测试。这里给 ctest 的命令行参数：-R <测试名称> : 执行单个测试-VV：打印详细输出测试执行结果如下：$ ctest -R Usage -VV UpdatecTest Configuration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl UpdateCTestConfiguration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl Test project /home/stephan/Documents/cpp_testing sample/build Constructing a list of tests Done constructing a list of tests Updating test list for fixtures Added 0 tests to meet fixture requirements Checking test dependency graph... Checking test dependency graph end 在这里我执行了名为 Usage 的测试。它以无参数的方式调用 Producer：test 3 Start 3: Usage 3: Test command: /home/stephan/Documents/cpp testing sample/build/Producer 输出不匹配 [^[0-9]+] 的正则模式，测试未通过。3: Enter the number of elements as argument 1/1 test #3. Usage ................ Failed Required regular expression not found. Regex=[^[0-9]+] 0.00 sec round. 0% tests passed, 1 tests failed out of 1 Total Test time (real) = 0.00 sec The following tests FAILED: 3 - Usage (Failed) Errors while running CTest $ 如果想要执行所有测试（包括那些用 GoogleTest 生成的），切换到 build 目录中，然后运行 ctest 即可：在 VSCodium 中可以通过点击信息栏的黄框处来调用 CTest。如果所有测试都通过了，你会看到如下输出：使用 Git 钩子进行自动化测试目前为止，运行测试是开发者需要额外执行的步骤，那些不能通过测试的代码仍然可能被提交和推送到代码仓库中。利用 Git 钩子 可以自动执行测试，从而防止有瑕疵的代码被提交。切换到 .git/hooks 目录，创建 pre-commit 文件，复制粘贴下面的代码：#!/usr/bin/sh (cd build; ctest --output-on-failure -j6) 然后，给文件增加可执行权限：$ chmod +x pre-commit 这个脚本会在提交之前调用 CTest 进行测试。如果有测试未通过，提交过程就会被终止：只有所有测试都通过了，提交过程才会完成：这个机制也有一个漏洞：可以通过 git commit --no-verify 命令绕过测试。解决办法是配置构建服务器，这能保证只有正常工作的代码才能被提交，但这又是另一个话题了。总结本文提到的技术实施简单，并且能够帮你快速发现代码中的问题。做单元测试可以提高代码质量，同时也不会打断你的工作流。GoogleTest 框架提供了丰富的特性以应对各种测试场景，文中我所提到的只是一小部分而已。如果你想进一步了解 GoogleTest，我推荐你阅读 GoogleTest Primer。（题图：MJ/f212ce43-b60b-4005-b70d-8384f2ba5860）via: https://opensource.com/article/22/1/unit-testing-googletest-ctest作者：Stephan Avenwedde 选题：lujun9972 译者：toknow-gh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
OpenWrt：一个开源的家用路由器固件替代品
OpenWrt 是一个基于 Linux 的开源操作系统，主要针对嵌入式网络设备。如果你在家里阅读这篇文章，你可能是用一个 LTE/5G/DSL/WIFI 路由器联网的。这种设备通常负责在你的本地设备（智能手机、PC、电视等）之间路由数据包，并通过内置的调制解调器提供对 WWW 的访问。你家里的路由器很可能有一个基于网页的界面，用于配置该设备。这种界面往往过于简单，因为它们是为普通用户制作的。如果你想要更多的配置选项，但又不想花钱买一个专业的设备，你应该看看其他的固件，如 OpenWrt。OpenWrt 的特点OpenWrt 是一个基于 Linux 的、针对嵌入式网络设备的开源操作系统。它主要用于替代各种家用路由器上的原始固件。OpenWrt 具备一个好的路由器应该具备的所有有用功能，如 DNS 服务器（dnsmasq），WiFi 接入点（AP）和客户端功能，用于调制解调器功能的 PPP 协议，而且，与标准固件不同，这一切都是可以完全配置的。LuCI 网页界面OpenWrt 可以通过命令行（SSH）或使用 GUI 配置界面（LuCI）进行远程配置。LuCI 是一个用 Lua 编写的轻量级、可扩展的网页 GUI，它可以精确地配置你的设备。除了配置，LuCI 还提供了很多额外的信息，如实时图表、系统日志和网络诊断。LuCI 有一些可选的扩展，以增加更多的配置选择。可写文件系统它的另一个亮点是可写文件系统。原有的固件通常是只读的，而 OpenWrt 配备了一个可写的文件系统，这要归功于一个巧妙的解决方案，它将 OverlayFS 与 SquashFS/JFFS2 文件系统相结合，允许安装软件包以增强功能。在 OpenWrt 文档 中可以找到更多关于文件系统架构的信息。扩展OpenWrt 有一个相关的软件包管理器，opkg，它允许安装额外的服务，比如 FTP 服务器、DLNA 媒体服务器、OpenVPN 服务器、用于实现文件共享的 Samba 服务器、控制电话的 Asterisk 等等。当然，有些扩展需要适当的底层硬件资源。动机你可能想知道为什么要冒着对你的设备造成不可修复的损害和失去保修的风险，而尝试更换路由器制造商的固件。如果你的设备以你想要的方式工作，那么你可能不应该。永远不要碰一个正在运行的系统！但是，如果你想增强功能，或者你的设备缺乏配置选项，那么你应该看看 OpenWrt 是否可以成为一种补救措施。在我的例子中，我想要一个旅行用的路由器，当我在露营地的时候，我可以把它放在一个合适的位置，以便让其它设备与这个本地 WiFi 接入点（AP）保持良好连接。该路由器将作为一个普通的客户端连接到互联网，并广播它的 WiFi 接入点让我的其它设备连接到它。这样我就可以配置我的所有设备与这个路由器的接入点连接，当我在其他地方时我只需要改变路由器的客户端连接。此外，在一些露营地，你只能得到一个单一设备的访问代码，我可以通过这种设置来加强。作为我的旅行路由器，我选择 TP-Link TL-WR902AC 的原因如下：很小两根 WiFi 天线5V 电源（USB）低功耗成本效益高（你以 30 美元左右的价格得到它）为了了解它的尺寸，这里是它在树莓派 4 旁边的样子：尽管这个路由器带来了我所需要的所有硬件功能，但我很快发现，默认的固件并不能让我按照我想要的方式配置它。该路由器主要是作为一个 WiFi 接入点，它可以复制现有的 WiFi 网络或通过板载以太网接口将自己连接到网络。默认的固件对于这些使用情况是非常有限的。（LCTT 译注：此型号国内没有销售，它的特点之一是可以通过插入 3G/4G USB 网卡连接到互联网，但由于它不在国内销售，所以没有支持哪种国内 3G/4G USB 网卡的说明，我 查下来 似乎华为的 E3372h-320 是可用的。有相关实践的同学可以分享一下经验。国内销售的其它类似型号只能通过以太网口或 WiFi 连接到互联网，这种情况下，如果只能通过 3G/4G 连接互联网，那需要另外买一个随身 WiFi /移动路由器。）幸运的是，该路由器能够运行 OpenWrt，所以我决定用它来替换原来的固件。安装当你的 LTE/5G/DSL/WiFi 路由器符合 最低要求 时，很有可能在它上面运行 OpenWrt。下一步，你要查看 硬件表，检查你的设备是否被列为兼容，以及你要选择哪个固件包。OpenWrt 的 TP-Link TL-WR902AC 的页面还包括安装说明，其中描述了如何刷入内部存储器。刷入固件的过程在不同的设备之间可能会有所不同，所以我就不详细介绍了。简而言之，我必须通过将设备连接到一个具有特定 IP 地址的网络接口上的 TFTP 服务器，重命名 OpenWrt 固件文件，然后按复位按钮启动设备。配置一旦刷入成功，你的设备现在应该用新的固件启动了。现在启动可能需要更长的时间，因为与默认固件相比，OpenWrt 具有更多的功能。为了开始配置，需要在你的 PC 和路由器之间建立一个直接的以太网连接，OpenWrt 在此充当了一个 DHCP 服务器，并将你的 PC 的以太网适配器配置为一个 DHCP 客户端。在 Fedora Linux 上，要激活你的网络适配器的 DHCP 客户端模式，首先你必须通过运行找出连接的 UUID：$ nmcli connection show NAME UUID TYPE DEVICE Wired Conn 1 7a96b...27a ethernet ens33 virbr0 360a0...673 bridge virbr0 testwifi 2e865...ee8 wifi -- virbr0 bd487...227 bridge -- Wired Conn 2 16b23...7ba ethernet -- 选择你要修改的连接的 UUID，然后运行：$ nmcli connection modify <UUID> ipv4.method auto 你可以在 Fedora 联网维基 中找到更多关于这些命令的信息。在你连接到路由器后，打开一个网页浏览器并导航到 http://openwrt/。现在你应该看到 LuCI 的登录管理器：使用 root 作为用户名，并将密码留空。配置 WiFi 和路由要配置你的 WiFi 天线，请点击 “ 网络 Network ” 菜单并选择 “ 无线 Wireless ”。在我的设备上，上面的天线 radio0 工作在 2.4GHz 模式，并连接到名为 MOBILE-INTERNET 的本地接入点。下面的天线 radio1 工作在 5GHz，有一个相关的接入点，SSID 为 OpenWrt_AV。通过点击 “ 编辑 Edit ” 按钮，你可以打开设备配置，以决定该设备属于 LAN 还是 WWAN 网络。在我的例子中，接入点 OpenWrt_AV 属于 LAN 网络，客户端连接 MOBILE-INTERNET 属于 WWAN 网络。配置的网络在 “ 接口 Interfaces ” 面板的 “ 网络 Network ” 下列出。为了获得我想要的功能，网络流量必须在 LAN 和 WWAN 网络之间进行路由。路由可以在 “ 网络 Network ” 面板的 “ 防火墙 Firewall ” 部分进行配置。我没有在这里做任何改动，因为在默认情况下，网络之间的流量是被路由的，而传入的数据包（从 WWAN 到 LAN）必须通过防火墙。因此，你需要知道的是一个接口是属于 LAN 还是 (W)WAN。这个概念使它相对容易配置，特别是对初学者来说。你可以在 OpenWrt 联网基础 指南中找到更多信息。专属门户公共 WiFi 接入点通常受到 专属门户 的保护，你必须输入一个访问代码或类似的代码。通常情况下，当你第一次连接到接入点并试图打开一个任意的网页时，这种门户就会出现。这种机制是由接入点的 DNS 服务器实现的。默认情况下，OpenWrt 激活了一个安全功能，可以防止连接的客户端受到 DNS 重新绑定攻击。OpenWrt 的重新绑定保护也阻止了专属门户网站被转发到客户端，所以你必须禁用重新绑定保护，以便你可以到达专属门户网站。这个选项在 “ 网络 Network ” 菜单的 “ DHCP 和 DNS DHCP and DNS ” 面板中。尝试 OpenWrt由于升级到 OpenWrt，我得到了一个基于商品硬件的灵活的旅行路由器。OpenWrt 使你的路由器具有完全的可配置性和可扩展性，而且由于其制作精良的网页 GUI，它也适合初学者使用。甚至有一些 精选路由器 在出厂时已经安装了 OpenWrt。你还可以用很多 可用的软件包 来增强你的路由器的功能。例如，我正在使用 vsftp FTP 服务器，在连接的 U 盘上托管一些电影和电视剧。看看该 项目主页，在那里你可以找到许多切换到 OpenWrt 的理由。图片来自： Stephan Avenwedde，CC BY-SA 4.0via: https://opensource.com/article/22/7/openwrt-open-source-firmware作者：Stephan Avenwedde 选题：lkxed 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
多测量几次总比测量一次好。我掉到坑里，希望你可以不用。我想写一篇文章来演示“如何使用树莓派实现莫某的自动化”，或围绕树莓派的其他一些有趣、好奇或有用的应用。正如你可能从标题中意识到的那样，我不能再提供这样的文章了，因为我毁了我心爱的树莓派。树莓派是每个技术爱好者办公桌上的标准设备。因此，大量教程和文章告诉你可以用它做什么。这篇文章反而涵盖了阴暗面：我描述了你最好不要做的事情！电缆颜色在谈到实际的破坏点之前，我想提供一些背景。在房屋内外进行电气工作时，你必须处理不同颜色的电缆。在德国，每栋房子都连接到三相交流电网，你通常会发现以下电缆颜色：零线：蓝色（PE）地线：黄绿色（L1）火线 1：棕色（L2）火线 2：黑色（L3）火线 3：灰色例如，给灯接线时，你接零线（N，蓝色）和火线（L，有 1/3 的机会是棕色），它们之间的电压为 230V 交流电。连接树莓派今年早些时候，我写了一篇关于 OpenWrt，家用路由器固件的开源替代品 的文章。在文章中，我使用了 TP-link 路由器设备。但是，最初的计划是使用我的树莓派 4。我的想法是构建一个旅行路由器，我可以将其安装在我的大篷车中以改善露营地的互联网连接（我是那种离不开互联网的露营者）。为此，我在我的树莓派中添加了一个单独的 USB 无线网卡以连接第二个 Wifi 天线并安装了 OpenWrt。此外，我添加了一个 12V 至 5V DC/DC 转换器来连接大篷车中的 12V 接线。我用桌上的 12V 汽车电池测试了这个设置，它按预期工作。一切设置和配置完成后，我开始将其安装到我的大篷车中。在我的大篷车里，我找到了一根蓝色和一根棕色的电线，将它与 12V 至 5V DC/DC 转换器相连，将保险丝放回，然后……这个芯片，自己爆开了，它才是真正的降压变压器。我非常自信地认为蓝线是在 0V 电位上，棕色的是在 12V 上，我甚至没有测量。后来我了解到，蓝色的线是在 12V 上，而棕色的线是接地（这在汽车电子产品中很常见）。总结自从这次事故后，我的树莓派就再也启动不起来了。由于树莓派的价格飞涨，我不得不寻找替代品。幸运的是，我遇到了 TP-Link 旅行路由器，它也可以运行 Open-WRT 并且可以令人满意地完成它的工作。最后：多测量几次总比测量一次好。via: https://opensource.com/article/23/3/how-i-destroyed-my-raspberry-pi作者：Stephan Avenwedde 选题：lkxed 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
跟随这个演示，学习如何在 Rust 中使用文件系统模块。知道如何读写文件对各种用途都很有用。在 Rust 中，这项任务是通过标准库中的文件系统模块（std::fs）完成的。在这篇文章中，我将向你介绍如何使用这个模块。为了演示这项任务，我准备了一些示例代码，也可以在 GitHub 上找到。准备工作在使用 Rust 时，失败的函数会返回 Result 类型。尤其是文件系统模块会返回专门的类型 std::io::Result<T, Error>。有了这些知识，你可以从 main() 函数中返回相同的类型：fn main() -> std::io::Result<()> { /* ...code comes here... */ Rust 文件写入在 Rust 中执行文件的 I/O 操作是相对容易的。写入文件可以简化为一行：use std::fs; fs::write("favorite_websites.txt", b"opensource.com")?; Ok(()) 使用错误传播操作符 (?)，错误信息被传递到调用函数中，随后可以处理错误。由于 main() 是调用栈中唯一的其他函数，如果写操作失败，错误信息将被传递到控制台输出。fs::write 函数的语法是非常先进的。第一个参数是文件路径，它必须是 std::path::Path 类型。第二个参数是内容，它实际上是一个字节切片（[u8]）。Rust 将传递的参数转换为正确的类型。幸运的是，这些类型基本上是下面的例子中所处理的唯一类型。使用文件描述符类型 std::fs::File 可以实现对写操作更简洁的访问：let mut file = fs::File::create("favorite_websites.txt")?; file.write_all(b"opensource.com\n")?; Ok(()) 由于文件类型实现了 Write 特性，所以可以使用相关的方法来写入文件。然而，create 方法可以覆盖一个已经存在的文件。为了获得对文件描述符的更多控制，必须使用 std::fs::OpenOptions 类型。这提供了类似于其他语言中的打开模式：let mut file = fs::OpenOptions::new() .append(true) .open("favorite_websites.txt")?; file.write_all(b"sourceforge.net\n")?; Rust 文件读取适用于写的东西也适用于读。读取也可以通过简单的一行代码来完成：let websites = fs::read_to_string("favorite_websites.txt")?; 以上一行读取文件的内容并返回一个字符串。除了读取字符串，还有 std::fs::read 函数，如果文件包含二进制数据，该函数会将数据读成一个字节向量。下一个例子显示了如何将文件的内容读入内存，随后逐行打印到控制台：let file = fs::File::open("favorite_websites.txt")?; let lines = io::BufReader::new(file).lines(); for line in lines { if let Ok(_line) = line { println!(">>> {}", _line); } } 总结如果你已经熟悉了其他编程语言，你可能已经注意到没有 close- 函数（或类似的）来释放文件句柄。在 Rust 中，当相关变量超出作用域，文件句柄就会被释放。为了定义关闭行为，可以在文件表示的周围应用作用域 ({ })。我建议你熟悉 Read 和 Write 特性，因为你可以在许多其他类型中找到这个特性的实现。via: https://opensource.com/article/23/1/read-write-files-rust作者：Stephan Avenwedde 选题：lkxed 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
这是一个演示如何创建 POSIX 兼容的间隔定时器的教程。对开发人员来说，定时某些事件是一项常见任务。定时器的常见场景是看门狗、任务的循环执行，或在特定时间安排事件。在这篇文章中，我将演示如何使用 timer\_create(...) 创建一个 POSIX 兼容的间隔定时器。你可以从 GitHub 下载下面样例的源代码。准备 Qt Creator我使用 Qt Creator 作为该样例的 IDE。为了在 Qt Creator 运行和调试样例代码，请克隆 GitHub 上的仓库，打开 Qt Creator，在 “ 文件 File -> 打开文件或项目…… Open File or Project... ” 并选择 “CMakeLists.txt”：在 Qt Creator 中打开项目选择工具链之后，点击 “ 配置项目 Configure Project ”。这个项目包括三个独立的样例（我们在这篇文章中将只会用到其中的两个）。使用绿色标记出来的菜单，可以在每个样例的配置之间切换，并为每个样例激活在终端运行 “ 在终端中运行 Run in terminal ”（用黄色标记）。当前用于构建和调试的活动示例可以通过左下角的“ 调试 Debug ” 按钮进行选择（参见下面的橙色标记）。项目配置线程定时器让我们看看 simple_threading_timer.c 样例。这是最简单的一个。它展示了一个调用了超时函数 expired 的间隔定时器是如何被创建的。在每次过期时，都会创建一个新的线程，在其中调用函数 expired：#include <stdio.h> #include <stdlib.h> #include <time.h> #include <signal.h> #include <unistd.h> #include <string.h> #include <errno.h> void expired(union sigval timer_data); pid_t gettid(void); struct t_eventData{ int myData; }; int main() { int res = 0; timer_t timerId = 0; struct t_eventData eventData = { .myData = 0 }; /* sigevent 指定了过期时要执行的操作 */ struct sigevent sev = { 0 }; /* 指定启动延时时间和间隔时间 * it_value和it_interval 不能为零 */ struct itimerspec its = { .it_value.tv_sec = 1, .it_value.tv_nsec = 0, .it_interval.tv_sec = 1, .it_interval.tv_nsec = 0 }; printf("Simple Threading Timer - thread-id: %d\n", gettid()); sev.sigev_notify = SIGEV_THREAD; sev.sigev_notify_function = &expired; sev.sigev_value.sival_ptr = &eventData; /* 创建定时器 */ res = timer_create(CLOCK_REALTIME, &sev, &timerId); if (res != 0){ fprintf(stderr, "Error timer_create: %s\n", strerror(errno)); exit(-1); } /* 启动定时器 */ res = timer_settime(timerId, 0, &its, NULL); if (res != 0){ fprintf(stderr, "Error timer_settime: %s\n", strerror(errno)); exit(-1); } printf("Press ETNER Key to Exit\n"); while(getchar()!='\n'){} return 0; } void expired(union sigval timer_data){ struct t_eventData *data = timer_data.sival_ptr; printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid()); } 这种方法的优点是在代码和简单调试方面用量小。缺点是由于到期时创建新线程而增加额外的开销，因此行为不太确定。中断信号定时器超时定时器通知的另一种可能性是基于 内核信号。内核不是在每次定时器过期时创建一个新线程，而是向进程发送一个信号，进程被中断，并调用相应的信号处理程序。由于接收信号时的默认操作是终止进程（参考 signal 手册页），我们必须要提前设置好 Qt Creator，以便进行正确的调试。当被调试对象接收到一个信号时，Qt Creator 的默认行为是：中断执行并切换到调试器上下文。显示一个弹出窗口，通知用户接收到信号。这两种操作都不需要，因为信号的接收是我们应用程序的一部分。Qt Creator 在后台使用 GDB。为了防止 GDB 在进程接收到信号时停止执行，进入 “ 工具 （ Tools ） -> 选项 Options ” 菜单，选择 “ 调试器 Debugger ”，并导航到 “ 本地变量和表达式 Locals & Expressions ”。添加下面的表达式到 “ 定制调试助手 Debugging Helper Customization ”：handle SIG34 nostop pass Sig 34 时不停止你可以在 GDB 文档 中找到更多关于 GDB 信号处理的信息。接下来，当我们在信号处理程序中停止时，我们要抑制每次接收到信号时通知我们的弹出窗口:Signal 34 弹出窗口为此，导航到 “GDB” 标签并取消勾选标记的复选框:定时器信号窗口现在你可以正确的调试 signal_interrupt_timer。真正的信号定时器的实施会更复杂一些：#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <signal.h> #include <time.h> #include <unistd.h> #include <errno.h> #include <string.h> #define UNUSED(x) (void)(x) static void handler(int sig, siginfo_t *si, void *uc); pid_t gettid(void); struct t_eventData{ int myData; }; int main() { int res = 0; timer_t timerId = 0; struct sigevent sev = { 0 }; struct t_eventData eventData = { .myData = 0 }; /* 指定收到信号时的操作 */ struct sigaction sa = { 0 }; /* 指定启动延时的时间和间隔时间 */ struct itimerspec its = { .it_value.tv_sec = 1, .it_value.tv_nsec = 0, .it_interval.tv_sec = 1, .it_interval.tv_nsec = 0 }; printf("Signal Interrupt Timer - thread-id: %d\n", gettid()); sev.sigev_notify = SIGEV_SIGNAL; // Linux-specific sev.sigev_signo = SIGRTMIN; sev.sigev_value.sival_ptr = &eventData; /* 创建定时器 */ res = timer_create(CLOCK_REALTIME, &sev, &timerId); if ( res != 0){ fprintf(stderr, "Error timer_create: %s\n", strerror(errno)); exit(-1); } /* 指定信号和处理程序 */ sa.sa_flags = SA_SIGINFO; sa.sa_sigaction = handler; /* 初始化信号 */ sigemptyset(&sa.sa_mask); printf("Establishing handler for signal %d\n", SIGRTMIN); /* 注册信号处理程序 */ if (sigaction(SIGRTMIN, &sa, NULL) == -1){ fprintf(stderr, "Error sigaction: %s\n", strerror(errno)); exit(-1); } /* 启动定时器 */ res = timer_settime(timerId, 0, &its, NULL); if ( res != 0){ fprintf(stderr, "Error timer_settime: %s\n", strerror(errno)); exit(-1); } printf("Press ENTER to Exit\n"); while(getchar()!='\n'){} return 0; } static void handler(int sig, siginfo_t *si, void *uc) { UNUSED(sig); UNUSED(uc); struct t_eventData *data = (struct t_eventData *) si->_sifields._rt.si_sigval.sival_ptr; printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid()); } 与线程定时器相比，我们必须初始化信号并注册一个信号处理程序。这种方法性能更好，因为它不会导致创建额外的线程。因此，信号处理程序的执行也更加确定。缺点显然是正确调试需要额外的配置工作。总结本文中描述的两种方法都是接近内核的定时器的实现。不过，即使 timer\_create(...) 函数是 POSIX 规范的一部分，由于数据结构的细微差别，也不可能在 FreeBSD 系统上编译样例代码。除了这个缺点之外，这种实现还为通用计时应用程序提供了细粒度控制。via: https://opensource.com/article/21/10/linux-timers作者：Stephan Avenwedde 选题：lujun9972 译者：FigaroCao 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
使用 C 扩展为 Python 提供特定功能。在前一篇文章中，我介绍了 六个 Python 解释器。在大多数系统上，CPython 是默认的解释器，而且根据民意调查显示，它还是最流行的解释器。Cpython 的独有功能是使用扩展 API 用 C 语言编写 Python 模块。用 C 语言编写 Python 模块允许你将计算密集型代码转移到 C，同时保留 Python 的易用性。在本文中，我将向你展示如何编写一个 C++ 扩展模块。使用 C++ 而不是 C，因为大多数编译器通常都能理解这两种语言。我必须提前说明缺点：以这种方式构建的 Python 模块不能移植到其他解释器中。它们只与 CPython 解释器配合工作。因此，如果你正在寻找一种可移植性更好的与 C 语言模块交互的方式，考虑下使用 ctypes 模块。源代码和往常一样，你可以在 GitHub 上找到相关的源代码。仓库中的 C++ 文件有以下用途：my_py_module.cpp: Python 模块 MyModule 的定义my_cpp_class.h: 一个头文件 - 只有一个暴露给 Python 的 C++ 类my_class_py_type.h/cpp: Python 形式的 C++ 类pydbg.cpp: 用于调试的单独应用程序本文构建的 Python 模块不会有任何实际用途，但它是一个很好的示例。构建模块在查看源代码之前，你可以检查它是否能在你的系统上编译。我使用 CMake 来创建构建的配置信息，因此你的系统上必须安装 CMake。为了配置和构建这个模块，可以让 Python 去执行这个过程：$ python3 setup.py build 或者手动执行：$ cmake -B build $ cmake --build build 之后，在 /build 子目录下你会有一个名为 MyModule. so 的文件。定义扩展模块首先，看一下 my_py_module.cpp 文件，尤其是 PyInit_MyModule 函数：PyMODINIT_FUNC PyInit_MyModule(void) { PyObject* module = PyModule_Create(&my_module); PyObject *myclass = PyType_FromSpec(&spec_myclass); if (myclass == NULL){ return NULL; } Py_INCREF(myclass); if(PyModule_AddObject(module, "MyClass", myclass) < 0){ Py_DECREF(myclass); Py_DECREF(module); return NULL; } return module; } 这是本例中最重要的代码，因为它是 CPython 的入口点。一般来说，当一个 Python C 扩展被编译并作为共享对象二进制文件提供时，CPython 会在同名二进制文件中（<ModuleName>.so）搜索 PyInit_<ModuleName> 函数，并在试图导入时执行它。无论是声明还是实例，所有 Python 类型都是 PyObject 的一个指针。在此函数的第一部分中，module 通过 PyModule_Create(...) 创建的。正如你在 module 详述（my_py_module，同名文件）中看到的，它没有任何特殊的功能。之后，调用 PyType\_FromSpec 为自定义类型 MyClass 创建一个 Python 堆类型 定义。一个堆类型对应于一个 Python 类，然后将它赋值给 MyModule 模块。注意，如果其中一个函数返回失败，则必须减少以前创建的复制对象的引用计数，以便解释器删除它们。指定 Python 类型MyClass 详述在 my\_class\_py\_type.h 中可以找到，它作为 PyType\_Spec 的一个实例：static PyType_Spec spec_myclass = { "MyClass", // name sizeof(MyClassObject) + sizeof(MyClass), // basicsize 0, // itemsize Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // flags MyClass_slots // slots }; 它定义了一些基本类型信息，它的大小包括 Python 表示的大小（MyClassObject）和普通 C++ 类的大小（MyClass）。MyClassObject 定义如下：typedef struct { PyObject_HEAD int m_value; MyClass* m_myclass; } MyClassObject; Python 表示的话就是 PyObject 类型，由 PyObject_HEAD 宏和其他一些成员定义。成员 m_value 视为普通类成员，而成员 m_myclass 只能在 C++ 代码内部访问。PyType\_Slot 定义了一些其他功能：static PyType_Slot MyClass_slots[] = { {Py_tp_new, (void*)MyClass_new}, {Py_tp_init, (void*)MyClass_init}, {Py_tp_dealloc, (void*)MyClass_Dealloc}, {Py_tp_members, MyClass_members}, {Py_tp_methods, MyClass_methods}, {0, 0} /* Sentinel */ }; 在这里，设置了一些初始化和析构函数的跳转，还有普通的类方法和成员，还可以设置其他功能，如分配初始属性字典，但这是可选的。这些定义通常以一个哨兵结束，包含 NULL 值。要完成类型详述，还包括下面的方法和成员表：static PyMethodDef MyClass_methods[] = { {"addOne", (PyCFunction)MyClass_addOne, METH_NOARGS, PyDoc_STR("Return an incrmented integer")}, {NULL, NULL} /* Sentinel */ }; static struct PyMemberDef MyClass_members[] = { {"value", T_INT, offsetof(MyClassObject, m_value)}, {NULL} /* Sentinel */ }; 在方法表中，定义了 Python 方法 addOne，它指向相关的 C++ 函数 MyClass_addOne。它充当了一个包装器，它在 C++ 类中调用 addOne() 方法。在成员表中，只有一个为演示目的而定义的成员。不幸的是，在 PyMemberDef 中使用的 offsetof 不允许添加 C++ 类型到 MyClassObject。如果你试图放置一些 C++ 类型的容器（如 std::optional），编译器会抱怨一些内存布局相关的警告。初始化和析构MyClass_new 方法只为 MyClassObject 提供一些初始值，并为其类型分配内存：PyObject *MyClass_new(PyTypeObject *type, PyObject *args, PyObject *kwds){ std::cout << "MtClass_new() called!" << std::endl; MyClassObject *self; self = (MyClassObject*) type->tp_alloc(type, 0); if(self != NULL){ // -> 分配成功 // 赋初始值 self->m_value = 0; self->m_myclass = NULL; } return (PyObject*) self; } 实际的初始化发生在 MyClass_init 中，它对应于 Python 中的 \_\_init\_\_() 方法：int MyClass_init(PyObject *self, PyObject *args, PyObject *kwds){ ((MyClassObject *)self)->m_value = 123; MyClassObject* m = (MyClassObject*)self; m->m_myclass = (MyClass*)PyObject_Malloc(sizeof(MyClass)); if(!m->m_myclass){ PyErr_SetString(PyExc_RuntimeError, "Memory allocation failed"); return -1; } try { new (m->m_myclass) MyClass(); } catch (const std::exception& ex) { PyObject_Free(m->m_myclass); m->m_myclass = NULL; m->m_value = 0; PyErr_SetString(PyExc_RuntimeError, ex.what()); return -1; } catch(...) { PyObject_Free(m->m_myclass); m->m_myclass = NULL; m->m_value = 0; PyErr_SetString(PyExc_RuntimeError, "Initialization failed"); return -1; } return 0; } 如果你想在初始化过程中传递参数，必须在此时调用 PyArg\_ParseTuple。简单起见，本例将忽略初始化过程中传递的所有参数。在函数的第一部分中，PyObject 指针（self）被强转为 MyClassObject 类型的指针，以便访问其他成员。此外，还分配了 C++ 类的内存，并执行了构造函数。注意，为了防止内存泄漏，必须仔细执行异常处理和内存分配（还有释放）。当引用计数将为零时，MyClass_dealloc 函数负责释放所有相关的堆内存。在文档中有一个章节专门讲述关于 C 和 C++ 扩展的内存管理。包装方法从 Python 类中调用相关的 C++ 类方法很简单：PyObject* MyClass_addOne(PyObject *self, PyObject *args){ assert(self); MyClassObject* _self = reinterpret_cast<MyClassObject*>(self); unsigned long val = _self->m_myclass->addOne(); return PyLong_FromUnsignedLong(val); } 同样，PyObject 参数（self）被强转为 MyClassObject 类型以便访问 m_myclass，它指向 C++ 对应类实例的指针。有了这些信息，调用 addOne() 类方法，并且结果以 Python 整数对象 返回。3 种方法调试出于调试目的，在调试配置中编译 CPython 解释器是很有价值的。详细描述参阅 官方文档。只要下载了预安装的解释器的其他调试符号，就可以按照下面的步骤进行操作。GNU 调试器当然，老式的 GNU 调试器（GDB） 也可以派上用场。源码中包含了一个 gdbinit 文件，定义了一些选项和断点，另外还有一个 gdb.sh 脚本，它会创建一个调试构建并启动一个 GDB 会话：GDB 使用脚本文件 main.py 调用 CPython 解释器，它允许你轻松定义你想要使用 Python 扩展模块执行的所有操作。C++ 应用另一种方法是将 CPython 解释器嵌入到一个单独的 C++ 应用程序中。可以在仓库的 pydbg.cpp 文件中找到：int main(int argc, char *argv[], char *envp[]) { Py_SetProgramName(L"DbgPythonCppExtension"); Py_Initialize(); PyObject *pmodule = PyImport_ImportModule("MyModule"); if (!pmodule) { PyErr_Print(); std::cerr << "Failed to import module MyModule" << std::endl; return -1; } PyObject *myClassType = PyObject_GetAttrString(pmodule, "MyClass"); if (!myClassType) { std::cerr << "Unable to get type MyClass from MyModule" << std::endl; return -1; } PyObject *myClassInstance = PyObject_CallObject(myClassType, NULL); if (!myClassInstance) { std::cerr << "Instantioation of MyClass failed" << std::endl; return -1; } Py_DecRef(myClassInstance); // invoke deallocation return 0; } 使用 高级接口，可以导入扩展模块并对其执行操作。它允许你在本地 IDE 环境中进行调试，还能让你更好地控制传递或来自扩展模块的变量。缺点是创建一个额外的应用程序的成本很高。VSCode 和 VSCodium LLDB 扩展使用像 CodeLLDB 这样的调试器扩展可能是最方便的调试选项。仓库包含了一些 VSCode/VSCodium 的配置文件，用于构建扩展，如 task.json、CMake Tools 和调用调试器（launch.json）。这种方法结合了前面几种方法的优点：在图形 IDE 中调试，在 Python 脚本文件中定义操作，甚至在解释器提示符中动态定义操作。用 C++ 扩展 PythonPython 的所有功能也可以从 C 或 C++ 扩展中获得。虽然用 Python 写代码通常认为是一件容易的事情，但用 C 或 C++ 扩展 Python 代码是一件痛苦的事情。另一方面，虽然原生 Python 代码比 C++ 慢，但 C 或 C++ 扩展可以将计算密集型任务提升到原生机器码的速度。你还必须考虑 ABI 的使用。稳定的 ABI 提供了一种方法来保持旧版本 CPython 的向后兼容性，如 文档 所述。最后，你必须自己权衡利弊。如果你决定使用 C 语言来扩展 Python 中的一些功能，你已经看到了如何实现它。via: https://opensource.com/article/22/11/extend-c-python作者：Stephan Avenwedde 选题：lkxed 译者：MjSeven 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
VirtualBox 能帮助任何人（即使是命令行新手）安装一个新的虚拟机。VirtualBox 能让任何人都可以轻松安装 Linux 虚拟机。你不需要有使用命令行的经验，就可以自己安装一个简单的 Linux 虚拟机。在虚拟机方面，我精通很多东西，但这篇文章将向新手展示如何安装一个 Linux 虚拟机。此外，这篇文章还概述了如何使用开源虚拟机管理程序 VirtualBox ，来运行以及安装一个测试目的的 Linux 系统。一些术语在开始之前，你需要了解在本安装教程中的两个操作系统（OS）之间的区别： 主机系统 host system ：这指的是你安装 VirtualBox 的操作系统（即本机的操作系统）。 客体系统 guest system ：这指的是你想要在主机系统之上运行的虚拟化系统。在输入/输出、网络、文件访问、剪贴板、音频和视频方面，主机系统和客体系统都必须能够交互。在本教程中，我将使用 Windows 10 作为 主机系统，Fedora 33 作为 客体系统。安装前的准备当我们谈论虚拟化时，实际上，我们指的是 硬件辅助虚拟化。硬件辅助虚拟化需要兼容的 CPU。过去十年来，几乎每个普通的 x86 CPU 都有这一功能。AMD 公司称这样的 x86 CPU 是具有 AMD 虚拟化技术（AMD-V） 的处理器，英特尔公司则称其是具有 Intel 虚拟化技术（VT-x） 的处理器。虚拟化功能增加了一些额外的 CPU 指令，你可以在 BIOS 中启用或禁用这些指令。在安装虚拟机之前：确保在 BIOS 中启用了虚拟化技术（AMD-V 或 VT-x）。下载并安装好 VirtualBox。准备虚拟机下载你要用的 Linux 发行版的镜像文件。下载 32 位还是 64 位的操作系统镜像都没有关系，因为在 32 位的主机系统上也可以启动 64 位的操作系统镜像（当然内存的使用会受限），反之亦然。注意事项： 如果可以的话，请下载附带有 逻辑卷管理器)（LVM）的 Linux 发行版。LVM 会将文件系统与物理硬盘驱动器解耦。如果你的空间不足时，这能够让你增加客体系统的硬盘驱动器的大小。现在，打开 VirtualBox，然后单击黄色的“ 新建 New ”按钮：接下来，配置客体操作系统允许使用多少内存：我的建议是：不要吝啬分配给客体操作系统使用的内存！当客体操作系统的内存不足时，客体系统将开始从随机存取存储器（RAM）向硬盘驱动器进行内存分页，这样会极大地恶化系统的性能和响应能力。如果底层的主机系统开始分页，你很可能不会注意到。对于具有图形化桌面环境的 Linux 工作站系统，我建议至少分配 4GB 内存。接下来，创建虚拟磁盘：虚拟磁盘的格式选择默认的选项 “VDI（VirtualBox 磁盘镜像）” 就可以了：在以下的窗口中，我建议选择“ 动态分配 dynamically allocated ”，因为这允许你在之后增加虚拟磁盘的大小。如果你选择了“ 固定大小 fixed size ”，磁盘的速度可能会更快，但你将无法修改虚拟磁盘的大小了：建议你使用附带有逻辑卷管理器（LVM）的 Linux 发行版，这样你就可以先创建一个较小的硬盘。如果之后你的客体系统的空间快用完了，你可以按需增加磁盘的大小。注意：我选择的客体系统为 Fedora，在 Fedora 的官网说明：Fedora 至少需要分配 20GB 的空闲磁盘空间。我强烈建议你遵守该规范。在这里，我选择了 8GB，以便稍后演示如何用命令行增加磁盘空间。如果你是 Linux 新手，或者对命令行没有经验，请依旧选择 20GB。创建好硬盘驱动器后，从 VirtualBox 主窗口的列表中选择新创建的虚拟机，然后单击“ 设置 Settings ”。在设置菜单中，点击“ 系统 System ”，然后选择“ 处理器 Processor ”标签。默认情况下，VirtualBox 只向客体系统分配一个 CPU 内核。在现代多核 CPU 计算机上，分配至少两个内核是没有任何问题的，这能显著地加快客体系统的速度：设置网络适配器接下来，要处理的是网络设置。默认情况下， VirtualBox 会创建一个 NAT 连接，这对于大多数情况来说，是没有问题、不用做其他更改的：你也可以创建多个网络适配器。以下是网络适配器最常见的类型：NAT：NAT 适配器能自动执行 网络地址转换。从外部看，主机和客体系统使用着相同的 IP 地址。你无法通过网络从主机系统内访问客体系统。（尽管，你也可以通过定义 端口转发，来访问某些服务。）当你的主机系统可以访问互联网时，则你的客体系统也可以访问互联网。NAT 不再需要进一步的配置。如果你只需要让客体系统接入互联网就可以的话，请选择 “NAT”。 桥接适配器 Bridged adapter ：在此配置中，客体系统和主机系统可以共享相同的物理以太网设备。这两个系统都将拥有独立的 IP 地址。从外部看，网络中会有两个独立的系统，它们共享相同的物理以太网适配器。这种设置更灵活，但需要更多的配置。如果你想要共享客体系统的网络服务的话，请选择 “桥接适配器”。 仅限主机的适配器 Host-only adapter ：在此配置中，客体系统只能与主机，或在同一主机上运行的其他客体系统相互通信。主机系统也可以连接到客体系统。但客体系统不能接入互联网或物理网络。如果你想要获得高安全性，请选择 “仅限主机的适配器”。分配操作系统镜像在设置菜单中，点击“ 存储 Storage ”，然后选择虚拟光盘驱动器。单击右侧的 “光盘”图标，然后点击“ 选择一个磁盘文件…… Choose a disk file… ”，然后分配你想要安装的、已下载的 Linux 发行版镜像：安装 Linux现在，就已经配置好了虚拟机。右上角关闭“ 设置 Settings ”菜单，返回主窗口。点击“绿色箭头”（即“开始”按钮）。虚拟机将从虚拟光盘驱动器启动，你将发现你已经进入到 Linux 发行版的安装程序中：设置分区安装程序将在安装过程中要求你提供分区信息。选择“ 自定义 Custom ”：注意： 我假设，你创建这一虚拟机的目的是为了测试。此外，你也无需关心客体系统的休眠，因为此功能会由 VirtualBox 来隐式地提供。因此，你可以省略交换分区，以节省主机系统的磁盘空间。请记住，如果你需要的话，你可以稍后自己添加交换分区。在 《Linux 系统交换空间的介绍》 这篇文章中，作者 David Both 进一步解释了如何添加交换分区，并选择交换分区正确的大小。Fedora 33 及之后更高的版本提供了一个 zram 分区，zram 分区可以用于存放分页和交换、并经过压缩过后的硬盘数据。zram 分区可以按需地调整大小，并且它比硬盘交换分区快得多。为了简单，我们只添加以下两个 挂载点 Mount Point ：保存更改，接下来我们继续安装。安装 VirtualBox 增强功能完成安装后，从硬盘驱动器启动，并登录到虚拟机。现在，你可以安装 VirtualBox 增强功能 VirtualBox Guest Additions ，其中包括特殊的设备驱动程序和系统应用程序，它们能提供以下功能：共享剪贴板共享文件夹更好的性能可自由扩展的窗口大小点击顶部菜单栏的“ 设备 Devices ”，然后选择“ 插入增强功能的 CD 镜像…… Insert Guest Additions CD image... ”，来安装 VirtualBox 增强功能：在大多数 Linux 发行版上，带有增强功能的 CD 镜像会自动挂载，并且能够在文件管理器中找到。Fedora 会问你是否要运行安装脚本。单击“ 运行 Run ”，并授予该安装进程 root 权限：安装完成后，需要重新启动系统。LVM：扩大磁盘空间我在之前给 Fedora 虚拟机分配了 8GB 硬盘空间，是一个愚蠢的决定，因为 Fedora 很快就会告警空间不足：正如我提到的，Fedora 官网建议安装时分配 20GB 的磁盘空间。因为 8GB 是 Fedora 33 安装启动就需要的最少空间。没有安装其他软件（除了 VirtualBox 增强功能）的一个新安装的系统就几乎占用了整个 8GB 的可用空间。这时候，不要打开 GNOME 软件中心或任何其他可能从互联网下载文件的东西。幸运的是，我选择了附带有 LVM 的 Fedora，这样我就可以用命令行轻松地修复这个问题。要增加虚拟机中文件系统的空间，你必须先增加主机系统上分配的虚拟硬盘驱动器。关闭虚拟机。如果你的主机系统运行的是 Windows，请打开终端，并进入到 C:\Program Files\Oracle\VirtualBox 目录下。使用以下命令，将磁盘大小扩大到 12,000MB：VBoxManage.exe modifyhd "C:\Users\StephanA\VirtualBox VMs\Fedora_33\Fedora_33.vdi" --resize 12000 然后启动虚拟机，并打开“ 磁盘 Disks ”工具。你可以看到你刚刚新创建且未分配的可用空间。选择“ 可用空间 Free Space ”，然后单击 “+” 按钮：现在，创建一个新的分区。选择你要使用的可用空间的大小：如果你不想在新分区上创建文件系统或任何其他内容，请选择“ 其他 Other ”：选择“ 无文件系统 No Filesystem ”：现在，磁盘空间应该如下图所示：虚拟机有了一个新的分区设备：/dev/sda3。通过输入 vgscan ，来检查你的 LVM 卷组，找到 fedora_localhost_live 这一 LVM 卷组 ：现在，已经万事俱备了。在新分区 /dev/sda3 中扩展卷组 fedora_localhost_live：vgextend fedora_localhost-live /dev/sda3 由于卷组比逻辑卷大，你可以增加逻辑卷的大小。命令 vgdisplay 显示了共有 951 个可用的物理扩展（PE）：将逻辑卷增加 951 个物理扩展：lvextend -l+951 /dev/mapper/fedora_localhost--live-root 在增加了逻辑卷后，最后一件事就是调整文件系统的大小：resize2fs /dev/mapper/fedora_localhost--live-root 这样磁盘空间就增加完成了！检查“ 磁盘使用分析器 Disk Usage Analyzer ”，你就可以看到扩展空间已经可用于文件系统了。总结使用虚拟机，你可以检查在一个特定的操作系统或一个特定版本的操作系统、软件是如何操作的。除此之外，你还可以尝试任何想测试的 Linux 发行版，而不必担心系统损坏。对于资深用户来说，VirtualBox 在测试、网络和模拟方面提供了广泛的可能性。via: https://opensource.com/article/21/6/try-linux-virtualbox作者：Stephan Avenwedde 选题：lujun9972 译者：chai001125 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出