分类 软件开发 下的文章

使用 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 会话:

Gnu 调试器(GDB)对于 Python C 和 C++ 扩展非常有用

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.jsonCMake Tools 和调用调试器(launch.json)。这种方法结合了前面几种方法的优点:在图形 IDE 中调试,在 Python 脚本文件中定义操作,甚至在解释器提示符中动态定义操作。

VSCodium 有一个集成的调试器。

用 C++ 扩展 Python

Python 的所有功能也可以从 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中国 荣誉推出

Rustlings 是由 Rust 团队维护的开源项目,旨在帮助你通过调试代码的方式来学习 Rust。

Ferris the crab under the sea, unofficial logo for Rust programming language

在我上一篇 关于 Rustup 的文章 中,我向你们展示了如何安装 Rust 工具链。但是,如果不能上手操作一下 Rust 的话下载工具链又有什么用?学习任何语言都包括阅读现有的代码和写很多的示例程序,这是精通一门语言的好方法。然而,我们还可以走第三条路:调试代码。

通过调试来学习牵扯到尝试去编译一个已经写好的(满是漏洞的)示例程序,理解编译器生成的错误信息,修复示例代码,然后再重新编译。重复这个过程直到代码能够成功被编译并运行。

Rustlings 是一个由 Rust 团队维护的开源项目,旨在帮助你通过调试代码来学习 Rust。它也会一路为你提供提示。如果你是一名 Rust 初学者,并且刚开始阅读或已经读完了 Rust 书籍,那么 Rustlings 就是理想的下一步。Rustllings 帮助你将运用书中所学,并转向开发更大的项目。

安装 Rustlings

我使用(并推荐)Fedora 电脑来体验 Rustlings,但是任何 Linux 发行版都可以。要安装 Rustlings,你必须下载并运行它的安装脚本。通常建议你以不具备任何特别权限的普通用户(非 root 用户)来运行脚本。

记住,你需要 Rust 工具链来使用 Rustlings。如果你还没有这些工具链,请参考我 关于 Rustup 的文章

当你准备好时,下载这个安装脚本:

$ curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh  > rustlings_install.sh
$ file rustlings_install.sh
rustlings_install.sh: Bourne-Again shell script, ASCII text executable

阅读脚本以了解它会做什么:

$ less rustlings_install.sh

然后运行安装:

$ bash rustlings_install.sh
[...]
Installing /home/tux/.cargo/bin/rustlings
Installed package `rustlings v4.8.0 (/home/tux/rustlings)` (executable `rustlings`)
All done!

运行 rustlings 以开始。

Rustlings 练习

你现在可以使用命令 rustlings。与标志 --help 一起执行来查看可选的选项。

$ rustlings --help

这个安装脚本也克隆了 Rustlings 的 Git 仓库,并安装了运行示例程序所需的依赖。你可以在 ruslings 下的 exercises 目录查阅这些示例程序。

$ cd rustlings
$ pwd
/home/tux/rustlings
$ ls
AUTHORS.md  Cargo.toml        CONTRIBUTING.md  info.toml install.sh README.md  target Cargo.lock  CHANGELOG.md  exercises install.ps1  LICENSE src tests
$ ls -m exercises/
advanced_errors, clippy, collections, conversions, enums, error_handling, functions, generics, if, intro, macros, mod.rs, 
modules, move_semantics, option, primitive_types, quiz1.rs, quiz2.rs, quiz3.rs, quiz4.rs, README.md, 
standard_library_types, strings, structs, tests, threads, traits, variables

从命令行列出所有练习

命令 ruslings 提供给你一个 list 命令用以展示每个示例程序,它的完整路径,以及状态 (默认为 “待定”)。

$ rustlings list
Name         Path                                 Status
intro1       exercises/intro/intro1.rs            Pending
intro2       exercises/intro/intro2.rs            Pending
variables1   exercises/variables/variables1.rs    Pending
variables2   exercises/variables/variables2.rs    Pending
variables3   exercises/variables/variables3.rs    Pending
[...]

在显示结尾处,你会有一个进度报告用来追踪进度。

Progress: You completed 0 / 84 exercises (0.00 %).

查看示例程序

命令 rustlings list 向你展示了现有的程序,所以你可以在任何时候查看这些程序的代码,你只需要将完整路径复制到你的终端作为命令 cat 或者 less 的参数:

$ cat exercises/intro/intro1.rs

验证你的程序

现在你可以开始调试程序了。你可以使用命令 verify 来做这件事。注意 Rustlings 选择了列表里的第一个程序(intro1.rs)并尝试去编译它,最后编译成功:

$ rustlings verify
Progress: [-----------------------------------] 0/84
✅ Successfully ran exercises/intro/intro1.rs!

You can keep working on this exercise,
or jump into the next one by removing the `I AM NOT DONE` comment:

 6 |  // Execute the command `rustlings hint intro1` for a hint.
 7 |  
 8 |  // I AM NOT DONE
 9 |

正如你从结果中所见,尽管示例代码成功编译了,你依然需要做一些工作。每个示例程序的源文件中都带有以下注释:

$ grep "NOT DONE" exercises/intro/intro1.rs
// I AM NOT DONE

虽然第一个程序的编译没有问题,除非你去掉注释 I AM NOT DONE,Rustlings 不会移到下一个程序。

来到下一个练习

一旦你从 intro1.rs 中去掉这些注释,你就可以通过再一次运行命令 rustlings verify 来到下一个练习。这一次,你会发现 Rustlings 尝试去编译这个系列中的下一个程序(intro2.rs),但是遇到了一个错误。你应该调试并修复这个问题,并前进。这是你理解为什么 Rust 说程序有漏洞的至关重要的一步。

$ rustlings verify
Progress: [>------------------------] 1/84
⚠️  Compiling of exercises/intro/intro2.rs failed! Please try again. Here's the output:
error: 1 positional argument in format string, but no arguments were given
 --> exercises/intro/intro2.rs:8:21
  |
8 |         println!("Hello {}!");
  |                         ^^

error: aborting due to previous error

来点提示

Rustlings 有一个非常好用的 hint 参数,这个参数会告诉你示例程序中哪里出错了,以及如何去修复它。你可以认为这是在编译错误信息基础之上,一个额外的帮助选项。

$ rustlings hint intro2
Add an argument after the format string.

基于以上提示,修复这个程序就很简单了。你只需要在语句 println 中加一个额外的参数。这个 diff 对比应该能帮你理解发生的变化:

< println!("Hello {}!", "world");
---
> println!("Hello {}!");

一旦你做出了修改,并从源代码中去掉了注释 NOT DONE,你可以再一次运行 rustlings verify 来编译并运行代码。

$ rustlings verify
Progress: [>-------------------------------------] 1/84
✅ Successfully ran exercises/intro/intro2.rs!

追踪进度

你无法在一天之内做完所有的练习,忘记练到哪也很常见。你可以执行命令 list 来查看你的练习状态。

$ rustlings list
Name         Path                                  Status
intro1       exercises/intro/intro1.rs             Done   
intro2       exercises/intro/intro2.rs             Done   
variables1   exercises/variables/variables1.rs     Pending
variables2   exercises/variables/variables2.rs     Pending
variables3   exercises/variables/variables3.rs     Pending
[...]

运行特定的练习

如果你不想从头开始并且想要跳过一些练习,Rustlings 允许你使用命令 rustlings run 来专注特定的练习。如此可以运行指定的程序而不需要验证之前的课程。例如:

$ rustlings run intro2
Hello world!
✅ Successfully ran exercises/intro/intro2.rs
$ rustlings run variables1

敲入练习名字可能会变得乏味,但 Rustlings 为你准备了便利的命令 next 用来移向系列中的下一个练习。

$ rustlings run next

替代命令 watch

如果你不想在每次修改后还要敲一次 verify,你可以在终端窗口中运行命令 watch,然后再继续修改源代码以解决问题。命令 watch 会检测到这些修改,然后重新编译以查看这些问题是否被解决。

$ rustlings watch

通过调试学习

Rust 编译器以提供非常有意义的错误信息而被熟知,这些错误信息会帮助你理解在你代码中的问题。这通常意味着更快的调试。Rustlings 是练习 Rust,学会阅读错误信息,并理解 Rust 语言的优秀途径。来看看 GitHub 上 Rustlings 5.0.0 的最新功能吧。

下载 Rust 速查表

via: https://opensource.com/article/22/7/learn-rust-rustlings

作者:Gaurav Kamathe 选题:lkxed 译者:yzuowei 校对:wxy

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

让你的开源项目文档充满活力,从而吸引各种经验水平的用户。

开源软件项目通常拥有非常多样化的用户人群。有些用户非常擅长使用该系统,并且只需要很少的文档。对于这些实力派用户,文档只需要提供必要的提示,并且可以包含更多的技术信息,比如说在 Shell 中运行的命令行。有些用户可能只是初学者。这些用户需要更多的帮助来设置系统并学习如何使用它。

写一个同时适合这两个用户群体的文档是令人生畏的。网站文档需要在 “提供详细的技术信息” 和 “提供更多的概述和指导” 之间寻求一个平衡。这是一个很难找到的平衡。如果你的文档不能同时满足这两个用户人群,那么考虑一下另外一个选择 —— 动态文档。

探索在网页中添加一点 JavaScript 使用户可以选择自己想看的内容。

构建你的内容

你可以把例程添加的你的文档中需要同时满足 专家 expert 初学者 novice 的地方。在这个例程中,我们可以使用一个虚构的名为 AwesmeProject 的音乐播放器。

你可以用 HTML 编写一个简短的安装文档,通过 HTML 的 class 功能同时为专家和初学者提供操作指南。

例如,你可以用下面的代码来为专家定义一个段落:

<p class="expert reader">

这同时指派了 “专家类” 和 “读者类”。你可以用下面的代码来为初学者创建一个相同的段落。

<p class="novice reader">

完整的 HTML 文件同时包含初学者的段落和专家的段落。

<!DOCTYPE html>

<html lang="en">

<head>

<title>How to install the software</title>
</head>

<body>

<h1>How to install the software</h1>

<p>Thanks for installing AwesomeProject! With AwesomeProject,
you can manage your music collection like a wizard.</p>

<p>But first, we need to install it:</p>

<p class="expert reader">You can install AwesomeProject from
source. Download the tar file, extract it, then run:
<code>./configure ; make ; make install</code></p>

<p class="novice reader">AwesomeProject is available in
most Linux distributions. Check your graphical package manager and search for AwesomeProject to install it.</p>

</body>

</html>

例子中的 HTML 文档没有与之关联的样式表,所以浏览器中会显示所有的段落。

Image of html in black text.

我们可在文档中添加一些简单的样式来为 读者 reader 专家 expert 或者 初学者 novice 突出任何元素。为了使不同的文本更容易区分,让我们把读者类的背景颜色设置成米白色,专家类的字体颜色设置为深红色,初学者的字体颜色则设置为深蓝色。

<!DOCTYPE html>

<html lang="en">

<head>

<title>How to install the software</title>

<style>

.reader {
background-color: ghostwhite;
}

.expert {
color: darkred;
}

.novice {
color: darkblue;
}

</style>

</head>

<body>

<h1>How to install the software</h1>

当你在浏览器中查看这个网页时,这些样式有助于突出这两个段落。安装指导的所有段落都有一个米白色背景,因为他们都有 读者 reader 这个类。第一个段落的字体是深红色的,这是由 专家 expert 这个类定义的。第二个段落的字体是深蓝色的,则是由 初学者 novice 这个类定义的。

Image of html in red and black text.

添加 JavaScript 控件

这些类的应用,使你可以添加一些简单的 JavaScript 函数,只显示其中一个内容块。一个方法是,首先给所有的读者类元素设置 display:none 。这会将内容隐藏,使其不会在页面上显示。然后,用函数将你想显示的类元素设置为 display:block :

<script>
function readerview(audience) {
  var list, item;
  // hide all class="reader"
  list = document.getElementsByClassName("reader");
  for (item = 0; item < list.length; item++) {
    list[item].style.display = "none";
  }
  // show all class=audience
  list = document.getElementsByClassName(audience);
  for (item = 0; item < list.length; item++) {
    list[item].style.display = "block";
  }
}
</script>

要在 HTML 文档中使用这个 JavaScript,你可以吧这个功能附加到一个按钮上。由于 readerview 函数需要一个 听众 audience (这应该是相对那个虚拟音乐播放器来说的)作为参数,你可以使用你想查看的听众类别来调用这个函数,可以是 读者 reader 专家 expert 或者 初学者 novice

<!DOCTYPE html>
<html lang="en">
<head>
<title>How to install the software</title>
  <style>
    .reader {
    background-color: ghostwhite;
    }

    .expert {
    color: darkred;
    }

    .novice {
    color: darkblue;
    }
  </style>
</head>

<body>

<script>
function readerview(audience) {
  var list, item;

  // hide all class="reader"
  list = document.getElementsByClassName("reader");

  for (item = 0; item < list.length; item++) {
    list[item].style.display = "none";
  }

  // show all class=audience
  list = document.getElementsByClassName(audience);

  for (item = 0; item < list.length; item++) {
    list[item].style.display = "block";
  }
}
</script>

<h1>How to install the software</h1>

<nav>

<button onclick="readerview('novice')">view novice text</button>

<button onclick="readerview('expert')">view expert text</button>

</nav>

<p>Thanks for installing AwesomeProject! With AwesomeProject,
you can manage your music collection like a wizard.</p>

<p>But first, we need to install it:</p>
<p class="expert reader">You can install AwesomeProject from
source. Download the tar file, extract it, then run
<code>./configure ; make ; make install</code></p>

<p class="novice reader">AwesomeProject is available in
most Linux distributions. Check your graphical package
manager and search for AwesomeProject to install it.</p>

</body>
</html>

有了这些设置,用户可以在网页上选择他们想看的文本。

Image of window that allows you to select between novice and expert text.

点击任何一个按钮都将只显示用户想要阅读的文本。例如,如果你点击了 “ 阅读初学者内容 view novice text ” 按钮,你就只会看到蓝色段落。

Image showing blue text when you press the novice button.

点击 “ 阅读专家内容 view expert text ” 按钮,就会隐藏初学者文本,只显示红色的专家文本。

Image of red text after the expert button is clicked.

将此扩展到你的文档

如果你的项目需要你为不同的听众编写多个操作文档,你可以考虑使用这种方法,一次发布,多次阅读。为所有的用户编写一个文档,是每个人都能很容易的发现和分享你项目的文档。而你也不必同时维护尽在细节上有所不同的多个文档。


via: https://opensource.com/article/22/12/dynamic-documentation-javascript

作者:Jim Hall 选题:lkxed 译者:duoluoxiaosheng 校对:wxy

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

即使是复杂的函数,也有几种方法可以单步调试,所以下次在排除代码故障时,可以尝试一下这些 GDB 技术。

调试器 是一个可以运行你的代码并检查问题的软件。GNU Debugger(GBD)是最流行的调试器之一,在这篇文章中,我研究了 GDB 的 step 命令和其他几种常见情况的相关命令。step 是一个被广泛使用的命令,但它有一些人们不太了解的地方,可能会使得他们十分困惑。此外,还有一些方法可以在不使用 step 命令的情况下进入一个函数,比如使用不太知名的 advance 命令。

1、无调试符号

考虑以下这个简单的示例程序:

#include <stdio.h>


int num() {
    return 2;
}

void bar(int i) {
    printf("i = %d\n", i);
}


int main() {
    bar(num());
    return 0;
}

如果你在没有 调试符号 debugging sysbols 的情况下进行编译(LCTT 译注:即在使用 gcc 编译程序时没有写 -g 选项),然后在 bar 上设置一个断点,然后尝试在这个函数内使用 step 来单步执行语句。GDB 会给出一个 没有行号信息 no line number information 的错误信息。

gcc exmp.c -o exmp
gdb ./exmp
(gdb) b bar
Breakpoint 1 at 0x401135
(gdb) r
Starting program: /home/ahajkova/exmp
Breakpoint 1, 0x0000000000401135 in bar ()
(gdb) step
Single stepping until exit from function bar,
which has no line number information.
i = 2
0x0000000000401168 in main ()

2、stepi 命令

但是你仍然可以在没有行号信息的函数内部单步执行语句,但要使用 stepi 命令来代替 stepstepi 一次只执行一条指令。当使用 GDB 的 stepi 命令时,先做 display/i $pc 通常很有用,这会在每一步之后显示 程序计数器 program counter 的值和相应的 机器指令 machine instruction

(gdb) b bar
Breakpoint 1 at 0x401135
(gdb) r
Starting program: /home/ahajkova/exmp
Breakpoint 1, 0x0000000000401135 in bar ()
(gdb) display/i $pc
1: x/i $pc
=> 0x401135 <bar+4>: sub $0x10,%rsp

在上述的 display 命令中,i 代表机器指令,$pc 表示程序计数器寄存器(即 PC 寄存器)。

使用 info registers 命令,来打印寄存器的内容,也是十分有用的。

(gdb) info registers
rax 0x2 2
rbx 0x7fffffffdbc8 140737488346056
rcx 0x403e18 4210200
(gdb) print $rax
$1 = 2
(gdb) stepi
0x0000000000401139 in bar ()
1: x/i $pc
=> 0x401139 <bar+8>: mov %edi,-0x4(%rbp)

3、复杂的函数调用

在带调试符号的 -g 选项,重新编译示例程序后,你可以使用行号在 mainbar 调用上设置断点,然后再单步执行 bar 函数的语句:

gcc -g exmp.c -o exmp
gdb ./exmp
(gdb) b exmp.c:14
Breakpoint 1 at 0x401157: file exmp.c, line 14.
(gdb) r
Starting program: /home/ahajkova/exmp
Breakpoint 1, main () at exmp.c:14
14 bar(num());

接下来,用 step,来单步执行 bar() 函数的语句:

(gdb) step
num () at exmp.c:4
4 return 2;

函数调用的参数需要在实际的函数调用之前进行处理,bar() 函数的参数是 num() 函数,所以 num() 会在 bar() 被调用之前执行。但是,通过 GDB 调试,你怎么才能如愿以偿地进入 bar() 函数呢?你可以使用 finish 命令,并再次使用 step 命令。

(gdb) finish
Run till exit from #0 num () at exmp.c:4
0x0000000000401161 in main () at exmp.c:14
14 bar(num());
Value returned is $1 = 2
(gdb) step
bar (i=2) at exmp.c:9
9 printf("i = %d\n", i);

4、tbreak 命令

tbreak 命令会设置一个临时断点。如果你不想设置永久断点,那么这个命令是很有用的。举个例子?,你想进入一个复杂的函数调用,例如 f(g(h()), i(j()), ...),在这种情况下,你需要一个很长的 step/finish/step 序列,才能到达 f 函数。如果你设置一个临时断点,然后再使用 continue 命令,这样就不需要以上的序列了。为了证明这一点,你需要像以前一样将断点设置在 mainbar 调用上。然后在 bar 上设置临时断点。当到达该临时断点后,临时断点会被自动删除。

(gdb) r
Starting program: /home/ahajkova/exmp
Breakpoint 1, main () at exmp.c:14
14 bar(num());
(gdb) tbreak bar
Temporary breakpoint 2 at 0x40113c: file exmp.c, line 9.

在调用 bar 的时候遇到断点,并在 bar 上设置临时断点后,你只需要使用 continue 继续运行直到 bar 结束。

(gdb) continue
Continuing.
Temporary breakpoint 2, bar (i=2) at exmp.c:9
9 printf("i = %d\n", i);

5、disable 命令

类似地,你也可以在 bar 上设置一个正常的断点,然后执行 continue,然后在不再需要第二个断点时,使用 disable 命令禁用这个断点,这样也能达到与 tbreak 相同的效果。

(gdb) b exmp.c:14
Breakpoint 1 at 0x401157: file exmp.c, line 14.
(gdb) r
Starting program: /home/ahajkova/exmp
Breakpoint 1, main () at exmp.c:14
14 bar(num());
(gdb) b bar
Breakpoint 2 at 0x40113c: file exmp.c, line 9.
(gdb) c
Continuing.
Breakpoint 2, bar (i=2) at exmp.c:9
9 printf("i = %d\n", i);
(gdb) disable 2

正如你所看到的,info breakpoints 命令在 Enb 列下显示为 n,这意味着这个断点已被禁用。但你也能在再次需要这个断点时,再启用它。

(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000401157 in main at exmp.c:14
breakpoint already hit 1 time
2 breakpoint keep n 0x000000000040113c in bar at exmp.c:9
breakpoint already hit 1 time
(gdb) enable 2
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040116a in main at exmp.c:19
breakpoint already hit 1 time
2 breakpoint keep y 0x0000000000401158 in bar at exmp.c:14
breakpoint already hit 1 time

6、advance 命令运行程序到指定的位置

另一个进入函数内部的方法是 advance 命令。你可以简单地用 advance bar,来代替 tbreak bar ; continue。这一命令会将程序继续运行到指定的位置。

advance 命令的一个很棒的地方在于:如果程序并没有到达你试图进入的位置,那么 GDB 将在当前函数运行完成后停止。因此,程序的执行会受到限制:

Breakpoint 1 at 0x401157: file exmp.c, line 14.
(gdb) r
Starting program: /home/ahajkova/exmp
Breakpoint 1, main () at exmp.c:14
14 bar(num());
(gdb) advance bar
bar (i=2) at exmp.c:9
9 printf("i = %d\n", i);

7、skip 命令

进入 bar 函数的另一种方式是使用 skip num 命令:

(gdb) b exmp.c:14
Breakpoint 1 at 0x401157: file exmp.c, line 14.
(gdb) skip num
Function num will be skipped when stepping.
(gdb) r
Starting program: /home/ahajkova/exmp
Breakpoint 1, main () at exmp.c:14
14 bar(num());
(gdb) step
bar (i=2) at exmp.c:9
9 printf("i = %d\n", i);

请使用 info skip 命令,来了解 GDB 跳过了哪些函数。num() 函数被标记为 y,表示跳过了 num() 函数:

(gdb) info skip
Num Enb Glob File RE Function
1 y n <none> n num

如果不再需要 skip,可以禁用(并稍后重新启用)或完全删除它。你可以添加另一个 skip,并禁用第一个 skip,然后全部删除。要禁用某个 skip,必须指定其编号(例如,skip disable 1),如果没有指定,则会禁用所有的 skip。启用或删除 skip 的工作原理相同:

(gdb) skip bar
(gdb) skip disable 1
(gdb) info skip
Num Enb Glob File RE Function
1 n n <none> n num
2 y n <none> n bar
(gdb) skip delete
(gdb) info skip
Not skipping any files or functions.

GDB 的 step 命令

使用 GDB 的 step 命令是调试程序的一个有用工具。即使是复杂的函数,也有几种方法可以单步调试这些函数,所以下次你在排除代码问题的时候,可以尝试一下这些 GDB 技术。


via: https://opensource.com/article/22/12/gdb-step-command

作者:Alexandra 选题:lkxed 译者:chai001125 校对:wxy

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

Rust FFI 和 bindgen 工具是为 Rust 调用 C 库而设计的。Rust 很容易与 C 语言对话,从而与任何其它可以与 C 语言对话的语言对话。

为什么要从 Rust 调用 C 函数?简短的答案就是软件库。冗长的答案则触及到 C 在众多编程语言中的地位,特别是相对 Rust 而言。C、C++,还有 Rust 都是系统语言,这意味着程序员可以访问机器层面的数据类型与操作。在这三个系统语言中,C 依然占据主导地位。现代操作系统的内核主要是用 C 来写的,其余部分依靠汇编语言补充。在标准系统函数库中,输入与输出、数字处理、加密计算、安全、网络、国际化、字符串处理、内存管理等等,大多都是用 C 来写的。这些函数库所代表的是一个庞大的基础设施,支撑着用其他语言写出来的应用。Rust 发展至今也有着可观的函数库,但是 C 的函数库 —— 自 1970 年代就已存在,迄今还在蓬勃发展 —— 是一种无法被忽视的资源。最后一点是,C 依然还是编程语言中的 通用语:大部分语言都可以与 C 交流,透过 C,语言之间可以互相交流。

两个概念证明的例子

Rust 支持 FFI( 外部函数接口 Foreign Function Interface )用以调用 C 函数。任何 FFI 所需要面临的问题是调用方语言是否涵盖了被调用语言的数据类型。例如,ctypes 是 Python 调用 C 的 FFI,但是 Python 并没有包括 C 所支持的无符号整数类型。结果就是,ctypes 必须寻求解决方案。

相比之下,Rust 包含了所有 C 中的原始(即,机器层面)类型。比如说,Rust 中的 i32 类对应 C 中的 int 类。C 特别声明了 char 类必须是一个字节大小,而其他类型,比如 int,必须至少是这个大小(LCTT 译注:原文处有评论指出 int 大小依照 C 标准应至少为 2 字节);然而如今所有合理的 C 编译器都支持四字节的 int,以及八字节的 double(Rust 中则是 f64 类),以此类推。

针对 C 的 FFI 所面临的另一个挑战是:FFI 是否能够处理 C 的裸指针,包括指向被看作是字符串的数组指针。C 没有字符串类型,它通过结合字符组和一个非打印终止符(大名鼎鼎的 空终止符)来实现字符串。相比之下,Rust 有两个字符串类型:String&str (字符串切片)。问题是,Rust FFI 是否能将 C 字符串转化成 Rust 字符串——答案是 肯定的

出于对效率的追求,结构体指针在 C 中也很常见。一个 C 结构体在作为一个函数的参数或者返回值的时候,其默认行为是传递值(即,逐字节复制)。C 结构体,如同它在 Rust 中的对应部分一样,可以包含数组和嵌套其他结构体,所以其大小是不定的。结构体在两种语言中的最佳用法是传递或返回引用,也就是说,传递或返回结构体的地址而不是结构体本身的副本。Rust FFI 再一次成功处理了 C 的结构体指针,其在 C 函数库中十分普遍。

第一段代码案例专注于调用相对简单的 C 库函数,比如 abs(绝对值)和 sqrt(平方根)。这些函数使用非指针标量参数并返回一个非指针标量值。第二段代码案例则涉及了字符串和结构体指针,在这里会介绍工具 bindgen,其通过 C 接口(头文件)生成 Rust 代码,比如 math.h 以及 time.h。C 头文件声明了 C 函数的调用语法,并定义了会被调用的结构体。两段代码都能在 我的主页上 找到。

调用相对简单的 C 函数

第一段代码案例有四处 Rust 对标准数学库内的 C 函数的调用:两处分别调用了 abs(绝对值)和 pow(幂),两处重复调用了 sqrt(平方根)。这个程序可以直接用 rustc 编译器进行构建,或者使用更方便的命令 cargo build

use std::os::raw::c_int;  // 32位
use std::os::raw::c_double; // 64位

// 从标准库 libc 中引入三个函数。
// 此处是 Rust 对三个 C 函数的声明:
extern "C" {
  fn abs(num: c_int) -> c_int;
  fn sqrt(num: c_double) -> c_double;
  fn pow(num: c_double, power: c_double) -> c_double;
}

fn main() {
  let x: i32 = -123;
  println!("\n{x}的绝对值是: {}.", unsafe { abs(x) });

  let n: f64 = 9.0;
  let p: f64 = 3.0;
  println!("\n{n}的{p}次方是: {}.", unsafe { pow(n, p) });

  let mut y: f64 = 64.0;
  println!("\n{y}的平方根是: {}.", unsafe { sqrt(y) });

  y = -3.14;
  println!("\n{y}的平方根是: {}.", unsafe { sqrt(y) }); //** NaN = NotaNumber(不是数字)
}

顶部的两个 use 声明是 Rust 的数据类型 c_intc_double,对应 C 类型里的 intdouble。Rust 标准模块 std::os::raw 定义了 14 个类似的类型以确保跟 C 的兼容性。模块 std::ffi 中有 14 个同样的类型定义,以及对字符串的支持。

位于 main 函数上的 extern "C" 区域声明了 3 个 C 库函数,这些函数会在 main 函数内被调用。每次调用都使用了标准的 C 函数名,但每次调用都必须发生在一个 unsafe 区域内。正如每个新接触 Rust 的程序员所发现的那样,Rust 编译器极度强制内存安全。其他语言(特别是 C 和 C++)作不出相同的保证。unsafe 区域其实是说:Rust 对外部调用中可能存在的不安全行为不负责。

第一个程序输出为:

-123的绝对值是: 123.
9的3次方是: 729.
64的平方根是: 8.
-3.14的平方根是: NaN.

输出的最后一行的 NaN 表示 不是数字 Not a Number :C 库函数 sqrt 期待一个非负值作为参数,这使得参数 -3.14 生成了 NaN 作为返回值。

调用涉及指针的 C 函数

C 库函数为了提高效率,经常在安全、网络、字符串处理、内存管理,以及其他领域中使用指针。例如,库函数 asctime(ASCII 字符串形式的时间)期待一个结构体指针作为其参数。Rust 调用类似 asctime 的 C 函数就会比调用 sqrt 要更加棘手一些,后者既没有牵扯到指针,也不涉及到结构体。

函数 asctime 调用的 C 结构体类型为 struct tm。一个指向此结构体的指针会作为参数被传递给库函数 mktime(时间作为值)。此结构体会将时间拆分成诸如年、月、小时之类的单位。此结构体的 字段 field 类型为 time_t,是 int(32位)和 long(64 位)的别名。两个库函数将这些破碎的时间片段组合成了一个单一值:asctime 返回一个以字符串表示的时间,而 mktime 返回一个 time_t 值表示自 “ 纪元 Epoch 以来所经历的秒数,这是一个系统的时钟和时间戳的相对时间。典型的纪元设置为 1900 年或 1970 年,1 月 1 日 0 时 0 分 0 秒。(LCTT 校注:Unix、Linux 乃至于如今所有主要的计算机和网络的时间纪元均采用 1970 年为起点。)

以下的 C 程序调用了 asctimemktime,并使用了其他库函数 strftime 来将 mktime 的返回值转化成一个格式化的字符串。这个程序可被视作 Rust 对应版本的预热:

#include <stdio.h>
#include <time.h>

int main () {
  struct tm sometime;  /* 时间被打破细分 */
  char buffer[80];
  int utc;

  sometime.tm_sec = 1;
  sometime.tm_min = 1;
  sometime.tm_hour = 1;
  sometime.tm_mday = 1;
  sometime.tm_mon = 1;
  sometime.tm_year = 1; /*LCTT 校注:注意,相对于 1900 年的年数*/
  sometime.tm_hour = 1;
  sometime.tm_wday = 1;
  sometime.tm_yday = 1;

  printf("日期与时间: %s\n", asctime(&sometime));

  utc = mktime(&sometime);
  if( utc < 0 ) {
    fprintf(stderr, "错误: mktime 无法生成时间\n");
  } else {
    printf("返回的整数值: %d\n", utc);
    strftime(buffer, sizeof(buffer), "%c", &sometime);
    printf("更加可读的版本: %s\n", buffer);
  }

  return 0;
}

程序输出为:

日期与时间: Fri Feb  1 01:01:01 1901
返回的整数值: 2120218157
更加可读的版本: Fri Feb  1 01:01:01 1901

(LCTT 译注:如果你尝试在自己电脑上运行这段代码,然后得到了一行关于 mktime 的错误信息,然后又在网上随便找了个在线 C 编译器,复制代码然后得到了跟这里的结果有区别但是没有错误的结果,不要慌,我的电脑上也是这样的。导致本地机器上 mktime 失败的原因是作者没有设置 tm_isdst,这个是用来标记夏令时的标志。tm_isdst 大于零则夏令时生效中,等于零则不生效,小于零标记未知。加入 sometime.tm_isdst = 0= -1 后应该就能得到跟在线编译器大致一样的结果。不同的地方在于结果第一行我得到的是 Mon Feb ...,这个与作者代码中 sometime.tm_wday = 1 对应,这里应该是作者写错了;第二行我和作者和网上得到的数字都不一样,这大概是合理的,因为这与机器的纪元有关;第三行我跟作者的结果是一样的,1901 年 2 月 1 日也确实是周五,这是因为 mktime 其实会修正时间参数中不合理的地方。至于夏令时具体是如何影响 mktime 这个问题,我能查到的只有 mktime 的计算受时区影响,更底层的原因我也不知道了。)

总的来说,Rust 在调用库函数 asctimemktime 时,必须处理以下两个问题:

  • 将裸指针作为唯一参数传递给每个库函数。
  • 把从 asctime 返回的 C 字符串转化为 Rust 字符串。

Rust 调用 asctime 和 mktime

工具 bindgen 会根据类似 math.htime.h 之类的 C 头文件生成 Rust 支持的代码。下面这个简化版的 time.h 就可以用来做例子,简化版与原版主要有两个不同:

  • 内置类型 int 被用来取代别名类型 time_t。工具 bindgen 可以处理 time_t 类,但是会生成一些烦人的警告,因为 time_t 不符合 Rust 的命名规范:time_t 以下划线区分 timet;Rust 更偏好驼峰式命名方法,比如 TimeT
  • 出于同样的原因,这里选择 StructTM 作为 struct tm 的别名。

以下是一份简化版的头文件,mktimeasctime 在文件底部:

typedef struct tm {
  int tm_sec;  /* 秒 */
  int tm_min;  /* 分钟 */
  int tm_hour;   /* 小时 */
  int tm_mday;   /* 日 */
  int tm_mon;  /* 月 */
  int tm_year;   /* 年 */
  int tm_wday;   /* 星期 */
  int tm_yday;   /* 一年中的第几天 */
  int tm_isdst;  /* 夏令时 */
} StructTM;

extern int mktime(StructTM*);
extern char* asctime(StructTM*);

bindgen 安装好后,mytime.h 作为以上提到的头文件,以下命令(% 是命令行提示符)可以生成所需的 Rust 代码并将其保存到文件 mytime.rs

% bindgen mytime.h > mytime.rs

以下是 mytime.rs 中的重要部分:

/* automatically generated by rust-bindgen 0.61.0 */

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct tm {
  pub tm_sec: ::std::os::raw::c_int,
  pub tm_min: ::std::os::raw::c_int,
  pub tm_hour: ::std::os::raw::c_int,
  pub tm_mday: ::std::os::raw::c_int,
  pub tm_mon: ::std::os::raw::c_int,
  pub tm_year: ::std::os::raw::c_int,
  pub tm_wday: ::std::os::raw::c_int,
  pub tm_yday: ::std::os::raw::c_int,
  pub tm_isdst: ::std::os::raw::c_int,
}

pub type StructTM = tm;

extern "C" {
  pub fn mktime(arg1: *mut StructTM) -> ::std::os::raw::c_int;
}

extern "C" {
  pub fn asctime(arg1: *mut StructTM) -> *mut ::std::os::raw::c_char;
}

#[test]
fn bindgen_test_layout_tm() {
  const UNINIT: ::std::mem::MaybeUninit<tm> = ::std::mem::MaybeUninit::uninit();
  let ptr = UNINIT.as_ptr();
  assert_eq!(
  ::std::mem::size_of::<tm>(),
  36usize,
  concat!("Size of: ", stringify!(tm))
  );
  ...

Rust 结构体 struct tm,跟原本在 C 中的一样,包含了 9 个 4 字节的整型字段。这些字段名称在 C 和 Rust 中是一样的。extern "C" 区域声明了库函数 astimemktime 分别需要只一个参数,一个指向可变实例 StructTM 的裸指针。(库函数可能会通过指针改变作为参数传递的结构体。)

#[test] 属性下的其余代码是用来测试 Rust 版的时间结构体的布局。通过命令 cargo test 可以进行这些测试。问题在于,C 没有规定编译器应该如何对结构体中的字段进行布局。比如说,C 的 struct tm 以字段 tm_sec 开头用以表示秒;但是 C 不需要编译版本遵循这个排序。不管怎样,Rust 测试应该会成功,而 Rust 对库函数的调用也应如预期般工作。

设置好第二个案例并开始运行

bindgen 生成的代码不包含 main 函数,所以是一个天然的模块。以下是一个 main 函数初始化了 StructTM 并调用了 asctimemktime

mod mytime;
use mytime::*;
use std::ffi::CStr;

fn main() {
  let mut sometime  = StructTM {
    tm_year: 1,
    tm_mon: 1,
    tm_mday: 1,
    tm_hour: 1,
    tm_min: 1,
    tm_sec: 1,
    tm_isdst: -1,
    tm_wday: 1,
    tm_yday: 1
  };

  unsafe {
    let c_ptr = &mut sometime; // 裸指针

    // 调用,转化,并拥有
    // 返回的 C 字符串
    let char_ptr = asctime(c_ptr);
    let c_str = CStr::from_ptr(char_ptr);
    println!("{:#?}", c_str.to_str());

    let utc = mktime(c_ptr);
    println!("{}", utc);
  }
}

这段 Rust 代码可以被编译(直接用 rustc 或使用 cargo)并运行。输出为:

Ok(
    "Mon Feb  1 01:01:01 1901\n",
)
2120218157

对 C 函数 asctimemktime 的调用必须再一次被放在 unsafe 区域内,因为 Rust 编译器无法对这些外部函数的潜在内存安全风险负责。此处声明一下,asctimemktime 并没有安全风险。调用的两个函数的参数是裸指针 ptr,其指向结构体 sometime (在 stack 中)的地址。

asctime 是两个函数中调用起来更棘手的那个,因为这个函数返回的是一个指向 C char 的指针,如果函数返回 Mon 那么指针就指向 M。但是 Rust 编译器并不知道 C 字符串 (char 的空终止数组)的储存位置。是内存里的静态空间?还是 heap asctime 函数内用来储存时间的文字表达的数组实际上是在内存的静态空间里。无论如何,C 到 Rust 字符串转化需要两个步骤来避免编译错误:

  • 调用 Cstr::from_ptr(char_ptr) 来将 C 字符串转化为 Rust 字符串并返回一个引用储存在变量 c_str 中。
  • c_str.to_str() 的调用确保了 c_str 是所有者。

Rust 代码不会增加从 mktime 返回的整型值的易读性,这一部分留作课外作业给感兴趣的人去探究。Rust 模板 chrono::format 也有一个 strftime 函数,它可以被当作 C 的同名函数来使用,两者都是获取时间的文字表达。

使用 FFI 和 bindgen 调用 C

Rust FFI 和工具 bindgen 都能够出色地协助 Rust 调用 C 库,无论是标准库还是第三方库。Rust 可以轻松地与 C 交流,并透过 C 与其他语言交流。对于调用像 sqrt 一样简单的库函数,Rust FFI 表现直截了当,这是因为 Rust 的原始数据类型覆盖了它们在 C 中的对应部分。

对于更为复杂的交流 —— 特别是 Rust 调用像 asctimemktime 一样,会涉及到结构体和指针的 C 库函数 —— bindgen 工具是优秀的帮手。这个工具会生成支持代码以及所需要的测试。当然,Rust 编译器无法假设 C 代码对内存安全的考虑会符合 Rust 的标准;因此,Rust 必须在 unsafe 区域内调用 C。


via: https://opensource.com/article/22/11/rust-calls-c-library-functions

作者:Marty Kalin 选题:lkxed 译者:yzuowei 校对:wxy

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

代码中的缩进指的是你在代码行的开头处的空格。像其他代码编辑器和 IDE 一样,VSCode 允许你自动缩进你的代码。

你可以设置使用制表符或空格或任何你喜欢的缩进方式。

听起来不错吧?让我们来看看怎么做。

在 VSCode 中启用自动缩进

你有多种方法可以实现这个目标。在本指南中,我将向你展示三种在 VSCode 中自动缩进代码的方法。

方法 1:配置全局用户设置

你可以通过命令模式访问全局用户设置。使用 Ctrl + Shift + P 来打开命令模式,搜索 Open User Settings 并按下回车:

access user setting from command pallet in vscode

它将打开设置。在那里,你需要搜索 Auto Indent,并在 “ 编辑器:自动缩进 Editor: Auto Indent ” 中选择 “ 全部 Full ”:

enable auto indent from global user settings in vscode

接着自动缩进会被启用,并应用于 VSCode 中每个打开的文件。

方法 2:在 VSCode 中使用检查器或格式化工具进行自动缩进

在这种方法中,你需要添加扩展程序,如代码格式化工具或者检查器,以获得理想的结果。

检查器 Linter 会识别代码中的错误,而 格式化工具 Formatter 只对你的代码进行格式化,使其更具可读性。你可以在 VSCode 市场 中搜索特定于你的编程语言的代码格式化器。

这里有一些我最喜欢的广泛流行语言的代码格式化工具和检查器:

  • C/C++:适用于 C 和 C++ 编程语言。
  • PHP:适用于 PHP。
  • markdownlint:适用于 Markdown 文件。
  • Python:适用于 Python 编程语言。
  • ESLint:适用于 JSON 和 javascript。
  • Beautify: 适用于 JavaScript、JSON、CSS、SASS 和 HTML。

当你为你喜欢的编程语言添加了格式化工具,你可以按 Ctrl + Shift + I 来格式化代码。

同样地,你也可以使用命令模式做同样的事情。按 Ctrl + Shift + P,并搜索 Format document,然后按下回车。

indent code in VSCode

方法 3:在保存文件时启用自动缩进功能

VSCode 允许你在保存你的代码时,通过一个小小的调整来格式化它。让我告诉你怎么做。

Ctrl + ,,它将打开用户设置提示。在那里,搜索 Format On Save

enable format on save option

从现在开始,当你保存文件时,你的文件将自动添加缩进。

总结

在本指南中,我解释了如何在 VSCode 中自动添加缩进。我建议使用第二种方法以获得更好的灵活性。

我希望你会发现本指南对你有帮助,如果你有任何疑问或建议,请在评论中告诉我。


via: https://itsfoss.com/auto-indent-vs-code/

作者:Sagar Sharma 选题:lkxed 译者:geekpi 校对:wxy

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