POSIX 多进程编程

并行计算方案包括多线程并行、多进程并行、异构并行和分布式并行。 这篇文章主要讲如何利用 POSIX 标准库(C语言)进行多进程并行计算,包括如何启动和终止子进程和进程间通信的两种方法:管道(pipe)与信号量(semaphore)。 至于其它的进程间通信方式,例如:共享内存(mmap)、网络(sockets)、信号(signal)和消息队列等,都是适用性更广也更复杂的技术,在这篇短文中无法涉及。

启动和终止子进程

使用 fork 创建子进程,使用 waitwaitpid 等待运行结束。

fork 创建子进程

#include <unistd.h>

pid_t fork(void);

返回值

  • 执行成功:在父进程中返回子进程的 pid_t, 在子进程中返回 0;
  • 执行失败:返回-1,并设置 errno
  • 在 GNU C 中 pid_t 就是 int

说明

  • fork 启动的子进程会获得与此时主进程完全一样的内存上下文,这意味着主进程此时可以访问的一切内容在子进程中都可用,甚至连指针地址都一样。 如果连续启动了两个子进程,在两次 fork 中有任何变量在主进程被修改了,前面的子进程看到的还是原来的值,后面的子进程看到的是新的值。 同时由于进程之间内存是隔离的,子进程修改任何内存都不会影响到其它子进程。 fork 对内存使用 copy-on-write 机制,没有修改的内存就访问同样的内容,但如果修改就会复制所有内存。

wait 等待子进程退出

#include <sys/wait.h>

pid_t wait(int *stat_loc);

参数

  • int *stat_loc 捕获子进程的退出值,设为 NULL 则忽略退出值。具体的返回参数参看 man 2 wait

返回值

  • 返回捕获到的子进程ID,失败返回-1,并设置 errno

waitpid 等待特定的子进程退出

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *stat_loc, int options);

参数

  • pid_t pid 等待特定的子进程
  • int *stat_loc 捕获子进程的退出值,设为 NULL 则忽略退出值, 与 wait 的参数相同
  • int options 等待的行为可以通过一系列选项来设置,具体的返回参数参看 man 2 waitpid,设成 0 则与 wait 行为相同。

返回值

  • 返回捕获到的子进程ID,失败返回-1

例子:创建一系列子进程并等待

在这个例子中创建了 10 个子进程,按照创建顺序给它们标号,让这些进程按照创建顺序的逆序打印自己的序号,主进程则会等待到所有子进程退出再结束。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{

  int n_process = 10;
  pid_t *pids = calloc(n_process, sizeof(pid_t));

  for (int i = 0; i < n_process; i++) {
    pids[i] = fork();
    if (pids[i] == 0) {
      /* in child process */
      sleep(n_process - i);
      printf("child %d\n", i);
      printf("child %d, pid_t: %d\n", i, pids[i]);
      /* pointer address is the same */
      printf("child %d, pids ptr: %p\n", i, pids);
      /* but value is different */
      printf("child %d, all pids: ", i);
      for (int i = 0; i < n_process; i++) {
        printf("%d ", pids[i]);
      }
      printf("\n");

      exit(0);
    }
  }

  /* in parent process */
  printf("all pids: \n");
  for (int i = 0; i < n_process; i++) {
    printf("%d ", pids[i]);
  }
  printf("\n");

  /* get parent pid */
  printf("pid: %d\n", getpid());

  /* wait subprocess to finish */
  for (int i = 0; i < n_process; i++) {
    /* wait(NULL); */
    waitpid(pids[i], NULL, 0);
  }

  free(pids);

  return 0;
}

管道 Pipe

POSIX 提供了单向管道,可以从一个进程向另一个进程单向地传输信息。

pipe 创建管道

#include <unistd.h>

int pipe(int fildes[2]);

参数

  • int fildes[2] 创建一个管道的两端,值是整数文件标识, fildes[0] 是管道的出口, fildes[1] 是管道的入口
  • 发送信息的进程应该先把管道出口关闭,而接收信息的进程应该先把管道入口关闭
  • 如果管道的两端在同一个进程里都关闭了,再向这个管道发送信息会产生 SIGPIPE 信号

返回值

  • 成功创建管道返回 0, 失败返回 -1 并设置 errno

例子:pingpong

两个进程之间互相发送数据,一个数据在两个管道之间来回传递,类似打乒乓球,这是 MPI 中最简单的例子

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define TOTAL_NUMBER 10

void child(int pipe_in, int pipe_out) {
  pid_t pid = getpid();
  printf("start pid: %d\n", pid);

  int recv_count = 0;
  int send_count = 0;
  char buf[100] = {0};
  int number = 0;

  while (recv_count < TOTAL_NUMBER || send_count < TOTAL_NUMBER) {

    /* clear buf */
    for (int i = 0; i < 100; i++) {
      buf[i] = 0;
    }

    /* read from pipe_in into buf */
    read(pipe_in, buf, 100);
    recv_count += 1;
    number = atoi(buf);
    printf("pid=%d: recv %d\n", pid, number);

    /* write to pipe_out */
    number += 1;
    sprintf(buf, "%d", number);
    write(pipe_out, buf, 100);
    printf("pid=%d: send %d\n", pid, number);
    send_count += 1;

  }

}

int main (void)
{
  int pipe_tochild[2];
  int pipe_fromchild[2];

  /* Create the pipes. */
  if (pipe (pipe_tochild) || pipe(pipe_fromchild))
    {
      return EXIT_FAILURE;
    }

  if (fork() == 0) {
    /* child process */
    close(pipe_tochild[1]);
    close(pipe_fromchild[0]);
    child(pipe_tochild[0], pipe_fromchild[1]);
    close(pipe_tochild[0]);
    close(pipe_fromchild[1]);
    exit(0);
  }
  /* parent process */
  close(pipe_tochild[0]);
  close(pipe_fromchild[1]);

  /* send first number */
  char buf[100];
  sprintf(buf, "%d", 1);
  write(pipe_tochild[1], buf, 100);

  /* loop */
  child(pipe_fromchild[0], pipe_tochild[1]);
  close(pipe_fromchild[0]);
  close(pipe_tochild[1]);

  wait(NULL);

  return 0;
}

信号量 System V Semaphores

信号量有两种,一个是旧的 System V 一个是标准 POSIX。System V 的支持更广,POSIX 标准在许多系统上都没有实现,所以这里只解释 System V 信号量。

semget 获得信号量集

#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

新建或访问已有的信号量集。

参数

  • key_t key 信号量集的键。每个信号量集都有唯一的键,自定义键名通过 ftok 创建,也可以设成 IPC_PRIVATE 来新建一个只有当前进程和子进程可见的信号量集
  • int nsems 信号量集中信号的个数
  • int semflg 控制创建和访问权限。
    • IPC_CREAT 表示创建新的信号量集
    • IPC_CREAT | IPC_EXCL 表示创建新的信号量集,并且当之前已经存在信号量集时失败
    • IPC_CREAT | 0666 创建时可以设置权限,与文件的权限规则相同

返回值

  • 成功时返回信号量集的ID semid ,失败时返回 -1 并设置 errno

semctl 配置信号量集

#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...); /* union semun sem_perm */

union semun {
    int     val;            /* value for SETVAL */
    struct  semid_ds *buf;  /* buffer for IPC_STAT & IPC_SET */
    u_short *array;         /* array for GETALL & SETALL */
};

semctl 的作用配置信号量集 semid 中的第 semnum 个参数的值。 配置行为由 cmd 指定,配置的值是可选的第四个参数 sem_union 。 配置的值很多,参看 man semctl.

参数

  • int semid 信号量集的 ID
  • int semnum 信号量集中的第几个信号
  • int cmd 要执行的操作,下面是常用的一些选项
    • SETVAL 把信号量的值设成 sem_union.val
    • SETALL 把所有的信号量的值都设成 sem_union.val
    • IPC_RMID 清除信号量
  • 可选的第四个参数 union semun sem_union 设置的值

返回值

  • 成功时返回非负值,与 cmd 设置有关
  • 失败时返回 -1,并设置 errno

semop 操作信号

#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

struct sembuf {
    u_short sem_num;        /* semaphore */
    short   sem_op;         /* semaphore operation */
    short   sem_flg;        /* operation flags */
};

semop 的作用是在信号量集 semid 上执行 nsops 个原子操作,每个操作由数组 sops 定义。 配置行为由 cmd 指定,配置的值是可选的第四个参数 sem_union 。 配置的值很多,参看 man semctl.

参数

  • int semid 信号量集的 ID
  • struct sembuf *sops 对信号的操作
    • u_short sem_num 对第几个信号操作
    • short sem_op 具体的操作
    • short sem_flg 控制操作的行为
  • size_t nsops 数组 sops 的长度

sem_op 的几种情况

  • >0 新的信号量值 = 旧的信号量值 + sem_op, 立即执行
  • =0 阻塞直到信号量的值变成 0
  • <0 阻塞直到 旧信号量 + sem_op >= 0

返回值

  • 成功时返回 0
  • 失败时返回 -1,并设置 errno

例子:互斥锁

这个例子里子进程和主进程一起对共享的变量 shared_int 数数,如果不加锁,最后输出的数字会比 2000000 少,也就是出现了数据竞争,而加锁之后就不会出现这种问题了。 可以注释掉 lockunlock 来自己观察一下。

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>
#include <sys/mman.h>


void lock(int semid) {
  struct sembuf sb = {0, -1, 0};
  semop(semid, &sb, 1);
}

void unlock(int semid) {
  struct sembuf sb = {0, 1, 0};
  semop(semid, &sb, 1);
}

int main() {

  int *shared_int = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
  if (shared_int == MAP_FAILED) {
    perror("mmap");
    exit(EXIT_FAILURE);
  }

  int semid = semget(IPC_PRIVATE, 1, 0666 | IPC_CREAT);
  union semun sem_union;
  sem_union.val = 1;
  semctl(semid, 0, SETVAL, sem_union);

  if (fork() == 0) {
    for (int i=0; i<1000000; ++i) {
      lock(semid);
      (*shared_int)++;
      unlock(semid);
    }
    exit(0);
  }


  for (int i=0; i<1000000; ++i) {
    lock(semid);
    (*shared_int)++;
    unlock(semid);
  }

  wait(NULL);

  printf("final, %d\n", *shared_int);

  semctl(semid, 0, IPC_RMID, sem_union);
  munmap(shared_int, sizeof(int));

  return 0;
}

TeNPy 调研

DMRG / MPS / 张量网络的开源库有很多,例如 TeNPy, ITensor, TensorKit, QSpace, DMRG++, uni10, PyTeNet, quimb, mpnum, ALPS project, TeNeS, SyTen, 等等。 这个系列文章将会从这些库中选择几个来学习和分析一下。

standards.png

Figure 1: 让我们再造个轮子🤣

TeNPyJohannes Hauschild主要开发并维护,Hauschild 的博士导师是 Frank Pollmann,现在在到处做博后。 考虑到 Hauschild 一作的文章没有几个,发表情况并不是很好,所以某种程度上他是在全职做 TeNPy 的开发。

安装

# 安装依赖
pip install -U pip numpy scipy cython setuptools wheel build
# 下载源码
git clone https://github.com/tenpy/tenpy.git
# 安装
cd tenpy
pip install -e .
# 测试
cd tests
pip install pytest
pytest

代码层级

TeNPy 的代码层级如图2

  • tenpy.simulations 是程序的入口,提供运行程序(进行读取参数、运行计算、执行测量、保存结果和中间过程等等)的功能
  • tenpy.algorithms 中实现了多种算法,包括 DMRG、iDMRG、TEBD和TDVP
  • tenpy.models 中实现了各种模型包括自旋、费米、波色和各种晶格结构。二维晶格通过映射到一维来实现。
  • tenpy.networks 中实现了 MPS 和 MPO 类型
  • tenpy.linalg 中实现了带有守恒量的张量以及迭代对角化算法 Lanczos 和 Arnoldi
  • tenpy.tools 中包括一些需要用到其它功能

overview.png

Figure 2: TeNPy 的代码层级

tenpy.linalg.np_conserved.Array

TeNPy 的基本数据结构是 tenpy.linalg.np_conserved.Array, 带有守恒荷的张量。 TeNPy 采用带有箭头的 MPS 记号来表示一个张量。 张量的每个指标有若干可能的量子数,这些信息记录在 Array.legs 属性里。 对于一个有 \(n\) 个指标的张量, Array.legs 是长度为 \(n\) 的数组,每个元素都是 tenpy.linalg.charges.LegCharge , 这里记录了这个指标所有可能的荷以及它是入指标还是出指标。 张量的属性 Array.qtotal 记录总量子数,只有那些和等于总量子数的块才会被保存。 每个保存的张量块的数据是数组 Array._data 的一个元素,它对于的量子数则是矩阵 Array._qdata 的一行。

class Array{
    legs   : List[LegCharge],
    qtotal : Charge,
    _data  : List[numpy.ndarray],
    _qdata : numpy.ndarray[len(​_data), len(legs)],
}

tenpy.networks.site.Site

tenpy.networks.site.Site 是一个格点或者一个物理指标所需要的数据。 它保存了物理指标所需要的信息 LegCharge, 以及作用在一个格点上的所有局域算符 Site.ops, 写模型的哈密顿量所需要的所有算符都是从这里得到的。 除了一般的算符之外还有 Jordan-Wigner 变换(如果需要)要用到的信息。

TeNPy 中实现的 Site 有如下这些

  • BosonSite
  • ClockSite
  • FermionSite
  • GroupedSite
  • SpinHalfFermionSite
  • SpinHalfHoleSite
  • SpinHalfSite
  • SpinSite

tenpy.networks.mps.MPS

对于 \(n\) 格点的链, tenpy.networks.mps.MPS 就是 \(n\) 个 Site 和 \(n\) 个 Array, 这些 Array 都是三阶张量. TeNPy 的 MPS 表示记录了每个块的形式(左正则、右正则、对称形式或者叫 Vidal 的 \(\Gamma \Lambda\) 形式)以及需要的奇异值。

class MPS{
    sites: List[Site],
    _B: List[Array],
    _S: List[numpy.ndarray[1d]],
    form: List[form],
}

tenpy.networks.mpo.MPO

tenpy.networks.mpo.MPO 与 MPS 表示类似,只不过每个块现在是四阶张量。

tenpy.models.model.Model

tenpy.models.model.Model 用来表示模型的哈密顿量,需要指定元胞格点的信息 Site 以及晶格结构如何映射到一维链 tenpy.models.lattice.Lattice. TeNPy 提供的构造模型的方法是通过 add_onsite, add_coupling 等设置哈密顿量里的各项,然后根据需要自动构造 MPO。

DMRG 算法

TeNPy 的抽象层次是

  • tenpy.algorithms.algorithm.Algorithm 提供运行、重启等功能
  • 然后由 tenpy.algorithms.mps_common.Sweep 提供扫描的抽象
  • 每个扫描步骤则使用 tenpy.algorithms.dmrg.DMRGEngine 它可以是 SingleSiteDMRGEngine 或者 TwoSiteDMRGEngine

wsl

WSL

配置 ssh

  1. 安装 openssh-server
  2. 编辑 /etc/ssh/sshd_config

    # change port
    Port 9922
    ListenAddress 0.0.0.0
    PasswordAuthentication yes
    
  3. 重启服务器 sudo service ssh restart
  4. windows 重定向端口

    netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=9922 connectaddress=172.23.241.25 connectport=9922
    
    1. 端口号就是 ssh 的
    2. wsl 的 ip 地址可以从 ifconfig 命令获得
    3. ifconfignet-tools 包中
  5. 在 windows 防火墙中添加端口

setuptools

setuptools: python 的打包工具

setuptools 是 pypa 的项目。

基本使用

pyproject.toml

PEP 517 要求所有 python 项目需要一个配置文件 pyproject.toml 其中描述项目的基本信息和依赖等。

    [build-system]
    requires = ["setuptools", "wheel"]
    build-backend = "setuptools.build_meta"

setup.cfg or setup.py

setup.cfg 是 setuptools 的 DSL,而 setup.py 用一般的 python 代码。

    from setuptools import setup

    setup(
        name='mypackage',
        version='0.0.1',
        packages=['mypackage'],
        install_requires=[
            'requests',
            'importlib; python_version == "2.6"',
        ],
    )
  • packages 指定 setuptools 要处理的包,可以使用 find_packages, find_namespace_packages 来自动找到所有包
  • install_requires 指定安装包需要的依赖, setuptools 可以自动从 pypa 安装依赖
  • setup_requires 指定依赖,但 setuptools 不会自动安装它

开发模式

使用开发模式,可以完成构建而不需要将文件复制到包目录。

  • 开发模式安装 setup.py develop
  • 开发模式卸载 setup.py develop --uninstall
  • 开发模式与 PEP 517 不兼容,所以用 pip 安装的方式是 pip install --editable .

cython

setuptools 会自动检查 cython 是否安装,如果没有,就会忽略所有 .pyx 文件。

PEP 517 要求在 pyproject.toml 中加入

   [build-system]
   requires=[..., "cython"]

为了兼容性,推荐在 setup.py 中也加入依赖 setup_requires = ['cython', ...]

setup.py

   from setuptools import setup
   from Cython.Build import cythonize

   setup(
       name='Hello world app',
       ext_modules=cythonize("hello.pyx"),
       zip_safe=False,
       setup_requires = ['cython'],
   )

apache

Apache HTTP 服务器

配置文件

参考: apache-httpd/configuring

配置文件叫做 httpd.conf 在 debian 是 /etc/apache2/apache.conf

语法

  • 每行一个指令,反斜线可以折行
  • 指令大小写不敏感,但参数大小写敏感
  • 指令的参数用空格分隔
  • 指令之前的空格都忽略,所以可以任意缩进
  • 变量用 Define 指令定义,用 ${VAR} 格式使用
  • 使用 apachectl configtest 检查配置文件的语法错误

配置

反向代理 jupyterlab

  • 需要的模块: a2enmod ssl rewrite proxy proxy_http proxy_wstunnel
  • jupyterlab 需要配置
    • c.ServerApp.allow_origin = '*'
    • c.ServerApp.base_url = '/jupyterlab'
    • c.ServerApp.port = 11413
<Location "/jupyterlab/">
    RewriteEngine On
    RewriteRule /jupyterlab/(.*) ws://127.0.0.1:11413/jupyterlab/$1 [P]
    RewriteRule /jupyterlab/(.*) http://127.0.0.1:11413/jupyterlab/$1 [P]

    ProxyPreserveHost on
    ProxyPass         http://127.0.0.1:11413/jupyterlab/
    ProxyPassReverse  http://127.0.0.1:11413/jupyterlab/
</Location>

PBLAS & ScaLAPACK & BLACS

PBLAS & ScaLAPACK & BLACS

ScaLAPACK 是基于 MPI 的并行版本 LAPACK, PBLAS 则是并行版本的 BLAS. 其中 PBLAS 是嵌入到 ScaLAPACK 里的,这与 LAPACK 经常嵌入到 BLAS 里刚好反过来

编译

  1. 从 github 下载 https://github.com/Reference-ScaLAPACK/scalapack/
  2. 修改 SLmake.inc 文件中的 FC, CC, BLASLIB, LAPACKLIB

示例

FC            = /home/beacon/app/openmpi/bin/mpif90
CC            = /home/beacon/app/openmpi/bin/mpicc 
NOOPT         = -O0 -std=legacy
FCFLAGS       = -O3 -std=legacy
CCFLAGS       = -O3


BLASLIB       = -L/usr/lib/x86_64-linux-gnu/openblas-openmp/ -lblas
LAPACKLIB     = -L/usr/lib/x86_64-linux-gnu/openblas-openmp/ -llapack

GCC 10 不兼容

由于 GCC 10 根据最新的 fortran 标准,要求参数类型匹配,所以老代码不给通过,可以添加 -fallow-argument-mismatch-std=legacy 来编译。

参考 Porting to GCC 10

BLACS

BLACS 的目的是提供一组在分布内存系统上的线性代数的标准通信接口,主要是作为 ScaLAPACK 的通信层。

除了 MPI 外, BLACS 还支持多种通信方案。当然现在 MPI 基本成为主流, 这部分作用已经意义不大了。

oneAPI/MKL/BLACS

基本概念

  • 进程网格和范围操作

    一个分布式机器上有 \(P\) 个进程,编号从 \(0\) 到 \(P-1\), 我们人为地将它们排列成 \(R\) 行 \(C\) 列的网格,用行列指标 \((i,j)\) 来表示其中一个进程。

    这样划分的好处在于线性代数操作时,二维数组被分散到 进程网格 (process grid)中,数据的行列与进程的行列有直接的对应,很适合编程。

    当进行的通信涉及两个以上的进程时,就将这种操作称作 范围操作 (scoped operations)。 基本的范围操作有

    • 行通信
    • 列通信
    • 全部通信
  • 上下文

    上下文 (context) 表示一个通信空间,每个进程网格都有自己的上下文,在同一个上下文内部的通信不会被其它上下文的覆盖。

    上下文的主要作用是可以将同一组进程标记成不同的进程网格,来方便进行通信操作。

    BLACS 中有关上下文的函数有

    • BLACS_GRIDINIT, BLACS_GRIDMAP
    • BLACS_GRIDEXIT, BLACS_EXIT
  • 基于数组的通信

    BLACS 中有两个基本的数据模型

    • 矩形矩阵

      一维向量是特殊的矩形矩阵。

      矩形矩阵是一个二维数组, MN 列,主维数是 LDA, 主维数就是相邻的两列数在内存中的间隔.

    • 梯形矩阵

      三角矩阵和对角矩阵是特殊的梯形矩阵。

      梯形矩阵的最大底边是 M,N 中最大的数,另一个是梯形的高,梯形的短底边是长底边和高的差。

      • UPLO: 梯形矩阵, 'U','L' 上下梯形
      • DIAG: 单位对角矩阵
  • 无编号通信

    BLACS 与其它通信层的区别之一是,BLACS不需要用户指定消息的编号 (例如 MPI 的 tag) ,因为编号的选择有时会导致编程的困难,所以 BLACS 通过一个特定的算法自动生成消息编号,用户可以用 SHIFT_RANGE 来将 BLACS 的消息编号限制在一个范围里,来和用户自定义的消息编号隔离。

    BLACS 的通信保证

    1. 接收端知道消息的来源
    2. 接收的顺序与发送的顺序一致

结构

BLACS 由 4 个部分构成

  • 点到点通信
  • 广播
  • 组合
  • 支持模块

函数参考

  • 命名规则
    • 点到点通信与广播: vXXYY2D
      • v 表示数据类型
      • XX 表示矩阵的形状
      • YY 表示通信的类型
    • 组合: vGZZZ2D
      • v 表示数据类型
      • ZZZ 表示操作类型
    • 支持模块: BLACS_<name>
    v 意义
    I 整数
    S 单精度
    D 双精度
    C 单精度复数
    Z 双精度复数
    XX 意义
    GE 矩形矩阵
    TR 梯形矩阵
    YY 意义
    SD 点到点发送
    RV 点到点接收
    BS 广播发送
    BR 广播接收
    ZZZ 意义
    AMX 最大绝对值
    AMN 最小绝对值
    SUM 求和

PBLAS

PBLAS 的函数类似 BLAS, 由于是作为 ScaLAPACK 的一部分分发的,所以不提供统一的 .h 头文件,得自己写。

oneAPI/MKL/PBLAS

ScaLAPACK

基本方法

Rust macro

Rust 宏

Rust 的宏分为两类,一种是声明宏 macro_rules! ,另一种是过程宏 #[...] .

声明宏 macro_rules!

声明宏是 Rust 中最常用的宏,通过对源码的模式匹配来实现功能。

语法

参考: reference/macro

  • #[macro_export] 导出宏
  • $x:expr 表明模式匹配一个 expr 类型的源码块,并用 $x 表示它
  • $( ... ),*$ 表明匹配括号中的内容 0 次或多次,每个重复的内容直接由 , 分隔

例子: vec!

    #[macro_export]
    macro_rules! vec {
        ( $( $x:expr ),* ) => {
            {
                let mut temp_vec = Vec::new();
                $(
                    temp_vec.push($x);
                )*
                    temp_vec
            }
        };
    }

过程宏

过程宏是一个函数,不过它的输入输出类型是词法对象 TokenStream ,由编译器在编译前调用。

crate

由于技术限制,目前过程宏必须分离在一个单独的 crate 中, 这个 crate 是 proc-macro 类型的

Cargo.toml

    [lib]
    proc-macro = true

derive

derive 宏用来为结构体等创建默认的 trait 实现。

下面的例子创建一个 HelloMacro trait 的默认实现宏。

  • 使用

    下面的例子展示了为 struct Pancakes 创建 trait HelloMacro 的默认实现的方法,这个 trait 中只有一个函数 hello_macro()

    src/main.rs

         use hello_macro::HelloMacro;
         use hello_macro_derive::HelloMacro;
    
         #[derive(HelloMacro)]
         struct Pancakes;
    
         fn main() {
             Pancakes::hello_macro();
         }
    
  • trait 定义

    hello_macro/src/lib.rs

         pub trait HelloMacro {
             fn hello_macro();
         }
    
  • 宏定义

    hello_macro_derive/Cargo.toml

         [lib]
         proc-macro = true
    
         [dependencies]
         syn = "1.0"
         quote = "1.0"
    

    hello_macro_derive/src/lib.rs

         extern crate proc_macro;
    
         use proc_macro::TokenStream;
         use quote::quote;
         use syn;
    
         #[proc_macro_derive(HelloMacro)]
         pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
             // Construct a representation of Rust code as a syntax tree
             // that we can manipulate
             let ast = syn::parse(input).unwrap();
    
             // Build the trait implementation
             impl_hello_macro(&ast)
         }
    
         fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
             let name = &ast.ident;
             let gen = quote! {
                 impl HelloMacro for #name {
                     fn hello_macro() {
                         println!("Hello, Macro! My name is {}!", stringify!(#name));
                     }
                 }
             };
             gen.into()
         }
    

Attribute-like

属性宏可以创建新的属性, 与 derive 宏的区别在于参数多了一个属性 attr, 也就是括号里面的部分,另一个参数 item 就是与 derive 宏一样的内容了。

  • 用法
         #[route(GET, "/")]
         fn index() { ... }
    
  • 宏定义
         #[proc_macro_attribute]
         pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { ... }
    

Function-like

函数宏可以定义像函数一样调用的宏。可以用来定义 DSL

  • 用法
         let sql = sql!(SELECT * FROM posts WHERE id=1);
    
  • 宏定义
         #[proc_macro]
         pub fn sql(input: TokenStream) -> TokenStream { ... }
    

syn: rust parser

syn 是 rust 代码的 parser,可以将源码字符串 TokenStream 转换成语法树 syn::DeriveInput

  • syn::DeriveInput

    对于输入 TokenStream 使用 parse_macro_input! 将它解析为语法树 DeriveInput ,之后就可以通过对语法树的操作生成新的语法树

  • syn::spanned::Spanned

    对重复结构的操作,使用 span() 配合 quote::qoute_spanned! 实现

quote: rust 代码模板

quotequote! 宏可以将 rust 语法数据结构变成 TokenStream

qoute_spanned! 宏可以将重复结构中的一个元素单独操作。

qoute 的宏类似于 macro_rules! 只是把 $ 换成 #

rust c ffi

Rust C FFI

rust-bindgen: rust 调用 c

rust-bindgen 是在编译时由头文件生成绑定代码的工具。由它封装的库称为 xxx-sys

常量宏

对于形如 #define XX (int)0 这样的有类型转换的宏是不能自动生成绑定的,可以在 wrapper.h 中写

    static const int _XX = XX;

来创建绑定。

libc: 系统库

c 类型

一般的类型在 std::os::raw 中,或者使用在 libc 中的重新绑定。

字符串

c 的字符串实际上是 &[u8] 数组,可以用 std::ffi::CStringstd::ffi::CStr, 其中 CString 拥有所有权, CStr 是借用。

指针

c 的常量指针 const int * 对应 *const i32, 一般指针 int * 对应 *mut i32. 两级指针 int ** 对应 *mut *mut int 以此类推。

结构体

rust 中定义与 c 兼容的结构体的方法为

    #[repr(C)]
    #[derive(Debug, Copy, Clone)]
    pub struct MyStruct {
        pub a: ::std::os::raw::c_int,
    }

sizeof

与 c 的 sizeof 相同的是 std::mem::sizeof

offsetof

使用 memoffsetoffset_of! 宏来获得。

如果不希望结构体内存对齐,使用 #[repr(packed)]

问题解决

传递 argc, argv

参考 stackoverflow

    extern crate libc;

    use libc::{c_char, c_int, c_void};
    use std::ffi::CString;

    extern "C" {
        fn foo(argc: *mut c_int, argv: *mut *mut *mut c_char);
    }

    fn main() {
        let mut c_args: Vec<*mut c_char> = std::env::args()
            .map(|arg| CString::new(arg).unwrap().into_raw())
            .collect();
        unsafe {
            let mut c_argc: c_int = c_args.len() as c_int;
            let mut c_argv: *mut *mut c_char = c_args.as_mut_ptr();

            foo(&mut c_argc as *mut c_int, &mut c_argv);
        }
    }

reStructuredText

reStructuredText

reST, 文件后缀 *.rst 是类似 markdown 的标记语言,是 python 的 Sphinx 默认语言。

基本语法

段落

与 python 类似,reST 中用缩进表示不同的层级。

行内标记

*something*               | 斜体
**something**             | 粗体
``code``                  | 行内代码
`title <http://to.link>`_ | 外部链接
`my link`_                | 分开的链接
.. _my link: http::/a.link

列表

    1. 编号列表
    2. 编号列表

    * 无编号列表
    * 无编号列表

    #. 还是编号列表
    #. 还是编号列表


    * 列表可以嵌套

      * 但是要空一行
      * 并且缩进

引用块

    引用块,末尾用双冒号::

      空一行

      并且缩进

    空一行就结束

表格

    完整的表格

    +------------------------+------------+----------+----------+
    | Header row, column 1   | Header 2   | Header 3 | Header 4 |
    | (header rows optional) |            |          |          |
    +========================+============+==========+==========+
    | body row 1, column 1   | column 2   | column 3 | column 4 |
    +------------------------+------------+----------+----------+
    | body row 2             | ...        | ...      |          |
    +------------------------+------------+----------+----------+

    简化的表格

    =====  =====  =======
    A      B      A and B
    =====  =====  =======
    False  False  False
    True   False  False
    False  True   False
    True   True   True
    =====  =====  =======

标题

# | parts
* | chapters
= | sections
- | subsections
^ | subsubsections
" | paragraphs

指令

reST 支持很多指令,这里是常用的几个

  • image 图片
  • [#footnote]_ 脚注
  • .. 注释

角色

reST 使用 :rolename:`content` 语法来做一些复杂的行内标记

:ref: 交叉引用

:math: 数学公式

Sphinx: Documentation Generator

Sphinx: 文档生成

创建与配置项目

sphinx-quickstart

使用这个命令,快速创建项目。跟随指导做一些选项。

生成文档

使用命令 sphinx-build -b html sourcedir builddir 或者 make html

基本配置

基本配置在 conf.py 文件中。这个文件就是一个python 脚本,可以执行各种python函数和导入其它库等。

配置参考configuration

写文档

文档结构

  1. index.rst 文件是文档的欢迎页面,其中包括
    1. 目录树 toctree

reStructuredText directives

rst 指令的格式中包括

  1. 参数:在指令名后面的冒号之后,每个指令可以有若干个参数
  2. 选项:在参数之后,选项的形式是 名-值 的列表,一行一个
  3. 内容:在参数之后空一行
  • toctree 目录

    参考:directives/toctree-directive

         .. toctree::
            :maxdepth: 2
    
            intro
            strings
            datatypes
            numeric
            (many more documents listed here)
    
    • 内容

      内容中每一行就是要链接到的文件名, 可以用 Net Title <filename> 重新指定显示的标题

    • 选项
      选项 作用
      :numbered: 给目录编号
      :caption: Table of Contents 目录的标题
      :name: mastertoc 设置 ref 用的名字
      :titlesonly: 只显示文件标题
      :glob: 可以使用 * 匹配很多文件
      :hidden: 链接,但不显示
      :includehidden: 只链接一级标题,隐藏其它的
      :maxdepth: 2 目录层级深度

Domains

为了对应 python/c++ 中的名字空间,防止函数名冲突,要把函数的文档写在 domain 里。

参考 domains

  • 基本语法

    可以一次生成两个函数

         .. py:function:: spam(eggs)
                          ham(eggs)
    
            Spam or ham the foo.
    

    如果一个函数很长,可以折行并加上 :noindex:

         .. py:function:: filterwarnings(action, message='', category=Warning, \
                                         module='', lineno=0, append=False)
            :noindex:
    

    默认的 domain 是 python,可以用 .. default-domain:: name 修改

  • 交叉引用

    基本语法是 :role:`title <target>` 这会引用 target 但显示的是 title

    1. 前面加 ! 不生成引用
    2. 前面加 波浪线 \~ 只会显示最后一个元素的引用 :py:meth:`\~Queue.Queue.get` 只显示 get
  • C++

Autodoc: 注释文档

通过 autodoc 可以从源码的注释生成文档。需要在 conf.pyextensions 中加入 'sphinx.ext.autodoc' . 之后就可以利用 autofunction automodule 等指令,将注释作为文档导入。

breathe

breathe 是通过 doxygen 生成 c/c++ 的文档的工具

基本使用

设置 conf.py

    breathe_projects_source = {
        "my_proj": ("../src", ["oneheader.h"]),
    }
    breathe_default_project = "my_proj"

就可以在 index.rst 中使用

    .. autodoxygenfile:: oneheader.h