标签 C 语言 下的文章

使用一个简单的计数程序比较古老的 C 语言和现代的 Go 语言。

Go 是一种现代编程语言,它很大程度上源自于 C 编程语言。因此,对于写 C 程序的程序员来说,Go 应该会感觉很熟悉。Go 让编写新程序变得容易,同时让 C 程序员感觉熟悉,但避免了 C 编程语言的许多常见陷阱。

本文比较了一个简单的 C 和 Go 程序,该程序将数字从一相加到十。由于这个程序只使用了小的数值,所以结果不会变得太大,因此只使用了普通的整数变量。像这样的循环在编程中非常常见,所以这个简单的程序很容易比较 C 和 Go。

如何在 C 中执行循环

C 语言中最基本的循环是 for 循环,它允许你对一组值进行迭代。for 循环的基本语法是:

for (起始条件 ; 结束条件 ; 每次迭代后执行的操作) { 循环内要执行的内容 ; }

你可以编写一个 for 循环,以打印从 1 到 10 的数字,将起始条件设置为 count = 1,将结束条件设置为 count <= 10。这样就以 count 变量等于 1 时开始循环。结束条件意味着只要 count 变量小于或等于 10 ,循环就会继续。

每次迭代之后,你使用 count = count + 1count 变量的值增加 1。在循环内部,你可以使用 printf 打印 count 变量的值:

for (count = 1; count <= 10; count = count + 1) {
  printf("%d\n", count);
}

C 程序中常见的惯例是 ++,它表示 “将某个值加一”。如果你写 count++,那就相当于 count = count + 1。大多数 C 程序员会使用 count++ 来编写 for 循环中每次迭代后要执行的操作,像这样:

for (count = 1; count <= 10; count++) {
  printf("%d\n", count);
}

这是一个示例程序,将从 1 到 10 的数字相加,然后打印结果。使用 for 循环对数字进行迭代,但不要打印数字,而是将数字添加到 sum 变量中:

#include <stdio.h>

int main() {
  int sum;
  int count;
  puts("adding 1 to 10 ..");
  sum = 0;

  for (count = 1; count <= 10; count++) {
    sum = sum + count;
  }

这个程序使用了两个不同的 C 函数来向用户打印结果。puts 函数打印引号中的字符串。如果你需要打印纯文本,使用 puts 是个不错的选择。

printf 函数 使用特殊字符在格式字符串中打印格式化的输出。printf 函数可以打印许多不同种类的值。关键字 %d 打印十进制(整数)值。

如果你编译并运行这个程序,你会看到这个输出:

adding 1 to 10 ..
The sum is 55

如何在 Go 中执行循环

Go 提供了与 C 中非常相似的 for 循环。C 程序中的 for 循环可以直接转换为 Go 的 for 循环,并具有相似的表示形式:

for count = 1; count <= 10; count++ {
  fmt.Printf("%d\n", count)
}

使用这个循环,你可以直接转换为 Go 的示例程序:

package main
import "fmt"

func main() {
  var sum, count int
  fmt.Println("adding 1 to 10 ..")

  for count = 1; count <= 10; count++ {
    sum = sum + count
  }
  fmt.Printf("The sum is %d\n", sum)
}

虽然上述方式在 Go 中是正确的,但它并不是最常用的 Go 写法。采用惯例是“使用与本地语言为人所知的表达方式”。任何语言的目标都是高效的沟通,编程语言也不例外。在不同的编程语言之间进行转换时,重要的是意识到尽管物似而意不同,一种编程语言中的典型写法在另一种编程语言中可能不完全相同。

为使用更符合惯例的 Go,你可以进行几个小修改:

  • 通过使用 += 操作符来将 sum = sum + count 更简洁地表达为 sum += count
  • 通过使用 分配并推断类型运算符 来表达 count := 1 而不是 var count int 跟着 count = 1:= 语法同时定义并初始化 count 变量。
  • count 的声明移到 for 循环的头中。这减少了一些认知负担,也通过减少程序员在任何时候都必须心里记着的变量数目来提高可读性。这个更改还通过在最接近其使用的地方和最小的范围中声明变量来增加安全性,从而减少了在代码不断演进的过程中对变量进行意外操作的可能性。

上述改动的组合将产生以下代码:

package main
import "fmt"

func main() {
  fmt.Println("adding 1 to 10 ..")
  var sum int
  for count := 1; count <= 10; count++ {
    sum += count
  }

  fmt.Printf("The sum is %d\n", sum)
}

你可以使用这个 Go.dev 的 链接 在 Go 试验场中尝试这个示例程序。

C 和 Go 相似但不同

通过在两种编程语言中编写相同的程序,你可以看到 C 和 Go 这两种语言虽然相似但仍然不同。将从 C 转换到 Go 时需要注意以下几点:

  • 在 C 中,每个程序指令都必须以分号结尾。这告诉编译器一个语句在哪里结束,下一个在哪里开始。在 Go 中,分号是有效的,但几乎总是可以推断出来。
  • 虽然大多数现代 C 编译器会为你将变量初始化为零值,但 C 语言规范指出,变量得到的是内存中的任意值。Go 值总是初始化为其零值。这有助于使 Go 成为一种更具内存安全的语言。这种差异在使用指针时变得更加有趣。
  • 注意 Go 程序包对导入标识符的使用方式。例如,fmt 是一个实现格式化输入和输出的函数,类似于 C 中的 stdio.h 中的 printfscanffmt 程序包在 pkg.go.dev/fmt 中有文档描述。
  • 在 Go 中,main 函数总是以退出代码 0 返回。如果你希望返回其他值,你必须调用 os.Exit(n),其中 n 通常为 1 以表示错误。这可以从任何地方调用,不仅仅是 main 函数,来终止程序。你可以在 C 中使用在 stdlib.h 中定义的 exit(n) 函数来实现相同的效果。

(题图:MJ/8f731484-2dc3-4bac-b895-cbc92a63b48b)


via: https://opensource.com/article/23/4/c-vs-go-programming-languages

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

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

在 FreeDOS 上使用 C 语言编程与在 Linux 上使用 C 语言编程非常类似。

当我第一次开始使用 DOS 时,我喜欢 DOS 自带的 BASIC 来编写游戏和其它一些有趣的程序。很长时间后,我才学习 C 编程语言。

我马上爱上了使用 C 语言做开发!它是一种简单易懂的编程语言,在编写有用的程序时,这给予我很大的灵活性。实际上,很多 FreeDOS 的核心实用程序都是使用 C 语言和汇编语言编写的。

因此,FreeDOS 的 1.3 RC4 包含一个 C 语言可能并不出人意料,此外还有其它编程语言的编译器。FreeDOS 的 1.3 RC4 LiveCD 包含两个 C 编译器:Bruce's C 编译器(一个简单的 C 编译器)和 OpenWatcom C 编译器 。在 Bonus CD 上,你也可以找到 DJGPP(一款基于 GNU 的 GCC 的 32 位 C 编译器)和 GCC 的 IA-16 移植(需要 386 或更好的 CPU 来编译,但是,生成的程序可以在低端系统上运行)。

在 FreeDOS 上使用 C 语言编程与在 Linux 上使用 C 语言编程非常类似,但是有两个例外:

  1. 你需要知道你使用了多少内存。 Linux 允许程序使用很多内存,但是 FreeDOS 有很多限制。DOS 程序只使用四种 内存模式(大、中、紧凑和小)中的其中一种,具体取决于它们需要多少内存。
  2. 你可以直接访问控制台终端。 在 Linux 上,你可以创建 文本模式 的程序,使用一个诸如 ncurses 之类的库来绘制终端屏幕。但是,DOS 允许程序访问控制台终端和视频硬件。这为编写更有趣的程序提供了极大的灵活性。

我喜欢在 GCC 的 IA-16 移植或 OpenWatcom 中编写我的 C 程序,具体取决于我正在编写的是哪种程序。OpenWatcom C 编译器更容易安装,因为它只是个单一的软件包。这就是为什么我们在 FreeDOS 的 LiveCD 中提供 OpenWatcom 的原因, 在你安装 FreeDOS 的 1.3 RC4 时,如果你选择 “ 完全的安装(包括安装应用程序和游戏) Full installation including applications and games ”,那么你也自动地安装 OpenWatcom。如果你选择安装 “ 纯 DOS 系统 Plain DOS system ”,那么,你将需要使用 FDIMPLES 软件包管理器来安装 OpenWatcom C 编译器。

 title=

在 FreeDOS 1.3 RC4 上安装 OpenWatcom

在 DOS 上使用 C 语言编程

你可以在 OpenWatcom 项目网站 找到文档和库指南,以学习 OpenWatcom C 编译器所提供的独特的关于 DOS 的 C 语言编程库。简单描述几个最有用的函数:

来自 conio.h 头文件:

  • int getch(void):从键盘上获取一个按下的单个按键
  • int getche(void):从键盘上获取一个按下的单个按键,并回显该按键

来自 graph.h 头文件:

  • _settextcolor(short color):设置打印文本时的颜色
  • _setbkcolor(short color):设置打印文本时的背景颜色
  • _settextposition(short y, short x):移动光标到行 y 和 列 x
  • _outtext(char _FAR *string):从当前光标位置开始,直接将一串字符打印到屏幕

DOS 只支持 16 种文本颜色 和 8 种背景颜色。你可以使用值 0(黑色)到 15(亮白色)来具体指定文本颜色,以及使用值 0(黑色)到 7(白色)来具体指定背景颜色:

  • 0:黑色
  • 1:蓝色
  • 2:绿色
  • 3:品蓝色
  • 4:红色
  • 5:品红色
  • 6:棕色
  • 7:白色
  • 8:亮黑色
  • 9:亮蓝色
  • 10:亮绿色
  • 11:亮品蓝色
  • 12:亮红色
  • 13:亮品红色
  • 14:黄色
  • 15:亮白色

一个花哨的 “Hello world” 程序

很多新开发者学习编写的第一个程序是为用户打印 “Hello world” 。我们可以使用 DOS 的 coniographics 库来制作一个更有趣的程序,并使用彩虹般的颜色打印 “Hello world” 。

在这个实例中,我们将遍历每种文本颜色,从 0(黑色)到 15(亮白色)。随着我们打印每一行,我们都将为下一行缩进一个空格。在我们完成后,我们将等待用户按下任意按键,然后,我们将重置屏幕并退出。

你可以使用任何文本编辑器来编写你的 C 源文件代码。我喜欢使用一些与众不同的编辑器,如 FreeDOS EditFreemacs,但是,我最近一直在使用 FED editor ,因为它提供 语法高亮 功能,使其很容易在我的程序源文件代码中看到关键字、字符串(LCCT 译注:C 语言中没有字符串)、变量。

 title=

使用 C 语言编写一个简单的测试程序

在你使用 OpenWatcom 编译前,你将需要设置 DOS 的 环境变量,以便 OpenWatcom 可以找到它的支持文件。OpenWatcom C 编译器软件包中包含了一个为你做这件事的设置 批处理文件\DEVEL\OW\OWSETENV.BAT。运行这个批处理文件可以自动为你的 OpenWatcom 设置环境变量。

在你的开发环境准备好后,你可以使用 OpenWatcom 编译器来编译这个 “Hello world” 程序。我已经将我的 C 源文件文件保存为 TEST.C ,因此,我可以输入 WCL TEST.C 来编译和连接该程序为一个名称为 TEST.EXE 的 DOS 可执行文件。在 OpenWatcom 的输出信息中,你将看到 WCL 实际上调用 OpenWatcom C 编译器(WCC)来编译,并调用 OpenWatcom 链接器(WLINK)来执行 对象/目标 object 链接阶段:

 title=

使用 OpenWatcom 编译测试文件

OpenWatcom 会打印一些无关的输出信息,这可能会使发现错误和警告变得困难。为了告诉编译器来抑制这些大量的额外信息,请在编译时使用 /Q(“Quiet”)选项:

 title= option to make OpenWatcom print less output")

在编译 C 源文件文件时,如果你没有看到任何错误信息,那么你现在就可以运行你的 DOS 程序了。这个 “Hello World” 示例的程序名称是 TEST.EXE 。在 DOS 命令行中输入 TEST 来运行新的程序,你应该会看到这个非常漂亮的输出:

 title=

C 语言是一种非常高效的编程语言,在像 DOS 之类的资源有限的系统上进行编程也可以很好的工作。在 DOS 上,你可以使用 C 语言来做更多的事。如果你是 C 语言的初学者,那么,你可以跟随我们在 FreeDOS 网站上的 《使用 C 语言编写 FreeDOS 程序》 的自学电子书,以及在 FreeDOS YouTube 频道 上的配套的 入门指南 how-to 系列视频,来自主学习 C 语言。

(题图:MJ:Legacy sci-fi computer programming::1.7 celestial::1 edison bulb::1 satellite imagery::1 wooden::1 in high resolution, very detailed, 8k)


via: https://opensource.com/article/21/6/program-c-freedos

作者:Jim Hall 选题:lujun9972 译者:robsean 校对:wxy

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

C 语言已不再仅仅是一种编程语言

有一篇论文说,《C 语言不是一种低级别的编程语言》。它不是说有其他编程语言比 C 语言更低级,C 语言经常被称赞为 “接近于裸金属”,是一种 “可移植的汇编语言”。它曾经是,但自 20 世纪 70 年代以来就不是了;现代计算机的底层计算模型与 C 所代表的模型完全不同,它是为 20 世纪 70 年代的 16 位微型计算机设计的。任何语言开发的程序都必须与操作系统对接,而操作系统几乎都是用 C 语言开发的。换句话说,即使你从来没有写过任何 C 语言的代码,你也必须处理 C 语言的变量,匹配 C 语言的数据结构和布局,用符号链接 C 语言的函数名称。

消息来源:The Register
老王点评:一种有趣的思考,或许是应该思考抛弃 C 语言构建的计算世界是什么样子的了。

百度以每年 10 万的价格出租的“虚拟人”

百度表示,自去年以来,它为客户做的虚拟人项目数量翻了一番,虚拟人的一些买家包括金融服务公司、地方旅游局和媒体。百度说,随着技术的改进,自去年以来,成本已经下降了约 80%。一个三维虚拟人每年将花费约为 10 万元,一个二维虚拟人则为 2 万元。预计到 2025 年,虚拟人产业总体上将保持每年 50% 的增长。

消息来源:CNBC
老王点评:至少有一个好处是,不必担心艺人丑闻了。

美国结束 3G 网络服务

Verizon 正在告诉客户,如果他们仍在使用不支持其较新网络技术的 3G CDMA 手机,“线路将被暂停而不计费,并将失去打电话、发短信或使用数据的能力”。Verizon 是美国三大无线运营商中最后一个关闭 3G 网络并将频谱重新用于更新技术的运营商。AT&T 和 T-Mobile 已于 2022 年分别关闭了其 3G 网络。事实上,Verizon 已经几次推迟了其 3G 网络的关闭。

消息来源:Fierce Wireless
老王点评:3G 都要被抛弃了,感觉 3G 还是昨天。

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中国 荣誉推出

systemd 创始人认为 Linux 的启动并不安全

已经加入了微软的 systemd 创始人 Lennart Poettering 认为,在启用了安全启动的硬件上,虽然启动过程(包括内核)是签名的,但下一步,即加载 initrd,却没有签名。他提议将 Linux 启动过程转移到加密签名的统一内核镜像,使用 TPM 2.0 硬件加强 Linux 系统启动过程的安全性。initrd 是“初始化内存磁盘”,恶意软件或入侵者可以在 initrd 中插入恶意代码,而且每次系统启动时都会加载该代码,即使该恶意代码的其他副本不存在于硬盘上。

消息来源:The Register
老王点评:想法是很好,但是这就是一个不透明的盒子。

Linux 内核开始为 800 Gbps 网络做准备

IEEE 已经发布了 800Gbps 网络的标准,而英伟达正在开发 800Gbps 网络硬件,其 Linux 内核驱动的开发人员确认,下一代英伟达 Spectrum ASIC 将支持 800Gbps 的速度。虽然 800Gbps 已经是目前最高端的 400Gbps 带宽的两倍,但以太网联盟今年的路线图已经将 1.6Tbps 网络作为未来几年内的计划。

消息来源:Phoronix
老王点评:Linux 内核在抛弃博物馆硬件的同时,越来越多的成为新硬件的第一批支持系统。

Fedora 40 正在为迁移到更现代的 C 语言标准做准备

1999 年发布的 C 语言标准(C99)删除了一些向后兼容的特性,然而,GCC 仍然默认接受这些过时的构造。但对这些结构体的支持让程序员感到困惑,并有可能影响 GCC 实现未来 C 标准中的功能。计划 2024 年发布的 GCC 14 编译器,正在考虑默认禁用对传统 C 语言结构的支持。LLVM 的开发者也正在考虑可能比这更早地禁用传统的 C 语言结构。Fedora 40 计划更主动地发现那些仍然依赖传统 C 语言行为的开源项目,以在 GCC 14 前就消除对旧的 C 语言标准的支持。

消息来源:Phoronix
老王点评:说是新的 C 语言标准,其实也有 20 多年了——都说程序员是喜新厌旧的,但是似乎都抱着自己用惯的语言不肯放手。

请下载我们的电子书获得在 Linux 和 FreeDOS 上 C 语言编程的提示和技巧。

有许多关于为什么 C 语言能够经久不衰的说法。或许是因为它语法简单明了。又或许是因为它常被认为是实用的语言,因为它不基于其他高级语言,可以在任何平台上编译运行。C 显然是一种强大的语言,并且我认为它经久不衰与它作为其他技术的基础的方式相关。这里有 5 项我喜爱的基于 C 语言的技术,希望它们能够帮助你更多的了解 C 语言。

1、GObject 和 GTK

C 语言不是面向对象编程的语言。它没有 class 关键字。 一些人用 C++ 进行面向对象编程,但是还有一些人坚持用 C 和 GObject 库。GObject 库为 C 语言提供了一个 class 结构体,GTK 项目以提供可通过 C 访问的工具包而闻名。没有 GTK ,就没有 GIMP (GTK 就是为此开发的)、GNOME 和其他成千上百流行的开源应用。

了解更多

GObject 和 GTK 是使用 C 开始进行 GUI 编程的绝佳方式。它们“装备精良”,可以让你用 C 语言进行图形应用的编程,因为开发者为你做了许多“繁重工作”。他们定义了类和数据类型,创建了工具包,你所要做的就是将所有东西放在一起。

2、Ncurses

如果 GTK 超过了你的需求,你或许认为一个 终端用户界面 terminal user interface (TUI)更适合你。Ncurses 库可以在终端创建“小部件”,创建一种在终端窗口上绘制图形的应用程序。你可以使用方向键控制界面,选择按钮和元素,就像不用鼠标来使用 GUI 应用一样。

了解更多

利用 Ncurses 库使用 C 语言写一个 猜数字 游戏。

3、Lua 和 Moonscript

Lua 是一种脚本语言,它可以使用内置的 C API 访问 C 语言库。它十分精巧、快捷以及简单,拥有约 30 个函数和少量内置库。你可以使用 Lua 进行系统自动化、游戏修改和脚本编写、使用 LÖVE 之类的前端进行游戏开发,或者使用 GTK 进行一般应用程序开发(例如 Howl 文本编辑器)。

了解更多

Lua 十分好的一点是你可以从它开始学习掌握基本的编程理念,然后当你有足够勇气直面基础编程语言时,再探索它的 C 语言 API 。另一方面,如果你只会 Lua ,那也没事儿。Lua 有很多的 外部库 ,使其成为各种开发方式的绝佳选择。

4、Cython

Lua 不是唯一带有 C 接口的编程语言。Cython 是一种编译器和编程语言,旨在使为 Python 编写 C 扩展就像编写 Python 代码一样容易。本质上,你可以编写 Python 并最终得到 C 语言程序。最简单的示例:

print("hello world")

创建一个 setup.py 脚本:

from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("hello.pyx")
)

运行该 setup 脚本:

$ python3 ./setup.py

最后你会在同一个目录中得到一个 hello.chello.cpython-39-x86_64-linux-gnu.so 文件。

了解更多

Cython 是 Python 的一个超集,支持 C 语言的函数和数据类型。它不可能帮你直接学习 C 语言,但它为希望学习 C 代码并将其集成到 Python 中的 Python 开发人员开辟了新的可能性。

5、FreeDOS

了解更多 C 语言的最好方式是编写 C 代码,没有什么比写你可以真正使用的代码更令人激动的了。FreeDOS 项目是 DOS 的开源实现, 而 DOS 是 Windows 的前身。或许你已经用过 FreeDOS 了,或者作为运行 BIOS 更新程序的便捷开源方法,或者在模拟器中玩经典的计算机游戏。你可以用 FreeDOS 做更多事情。它是学习 C 语言的理想平台,其中包含一系列工具,鼓励你编写自己的命令和简单(或不那么简单,如果你愿意)的应用程序。当然你可以在任何系统上写 C 代码,但是 FreeDOS 的便利可能会让你感到耳目一新。天空有极限,但即使在地面上,你也可以用 C 做一些非常有趣的事情。

下载电子书

你可以从我们编写的新 电子书 中学到更多 C 语言,并在我们的电子书中了解有关 FreeDOS 上 C 语言的更多信息。这些是编程文章的集合,可帮助你学习 C 语言,并演示如何以有用的方式用 C 写一些代码。

下载电子书

via: https://opensource.com/article/22/7/learn-c-linux

作者:Alan Smithee 选题:lkxed 译者:Donkey 校对:wxy

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