文章

手动编写VAPI

本文档旨在介绍如何编写将现有 C 库绑定到 Vala。如果该库使用的是 GLib,请勿遵循本文档。请阅读使用 GObject 自省生成 VAPI。库可能并不完全遵循 GLib 的编码实践,但修复库以便与 GObject 自省一起工作比编写手动绑定更好。

C 程序员是一群相当自由的人,某些程序可以根据程序员的心情以多种方式完成,而 Vala 则受到更多限制。本指南不可能涵盖 C 程序员编写的不同 API 的所有可能情况。您的工作就是理解 C API,并以对 Vala 友好的语义来呈现它。

本文件包含大量资料,一开始可能难以理解。学习本教程的实用方法是:

  1. 首先绑定一个枚举,因为枚举很容易测试。 一旦测试得到了预期的结果,你就知道构建过程成功了。这意味着需要学习“入门”部分和“枚举和标志”子部分。绑定枚举还引入了一个概念,即从 C 到 Vala 并没有一个直接的映射关系。

  2. 接下来绑定紧凑类的创建和销毁。 这意味着要学习“使用 Vala 的自动内存管理”部分,并开始理解 C 语言中的结构体在 Vala 中可以绑定为简单类型、结构体或紧凑类。可以通过查看 Vala 中的一行 C 代码(如new MyBoundCompactClass (); )来测试绑定。

  3. 绑定紧凑类的方法。 这是您的绑定开始变得有用的时候,也是对本文档进行概述的时候。一旦你有了一个概览,本文档将成为解决棘手的函数绑定问题的更多参考资料。

以上假设库是用面向对象的 C 语言编写的。但 C 绑定只是由结构体和函数组成,因此,足够详细地了解这一点才是本方法的目的。

先决条件

要编写绑定程序,请收集以下内容:

  • 带头文件的功能库副本
  • 库的文档,如果有的话
  • 源代码,如果可能的话
  • 可用作绑定测试的示例或教程

如果库是用 C++ 编写的,则无法将其绑定到 Vala,除非 C++ 库有单独的 C 绑定(如 LLVM)。

如果您使用的是 vim,不妨在 .vimrc 中添加以下内容:

1
:noremap <F8> "gyiwO[CCode (cname = "<ESC>"gpa")]<ESC>

它允许您插入一个属性,以便在光标位于符号上时按 F8 键,更轻松地重命名函数。

入门

VAPI 文件

检查程序库的开发包是否安装了pkg-config文件(扩展名为 .pc 文件)。如果有,请为您的 VAPI 文件取相同的名称。例如,libfoo.pc 应将 VAPI 命名为 libfoo.vapi。这样就可以自动获取库文件的详细信息,并将其传递给 C 编译器和链接器。

在开发 VAPI 时,针对绑定建立测试的典型命令是

1
valac --vapidir . --pkg libfoo program_using_libfoo.vala

--vapidir后面的点告诉valac在查找 VAPI 文件时包含当前目录。通过--pkg libfoo命令行开关,valac会查找名为libfoo.vapi 的 VAPI。请注意,.vapi后缀被去掉了。如果 VAPI 也与.pc文件同名,valac将查找并使用.pc文件提取相关库的详细信息,然后传递给 C 编译器和链接器。

VAPI 文件示例可在 Vala git 仓库的vapi 目录中找到。声明由vapigen生成的文件是通过 GObject 自省生成的,并非手动编写的绑定示例。

一旦您有了可运行的 VAPI 文件,即使它只是库功能的一个子集,也请考虑共享该文件。请参阅Vala额外的VAPI为什么要在上游分发绑定

署名和许可证

如果要通过主要资源库之一发布 VAPI,则需要版权声明。在开始编写绑定程序时处理这一手续可能更容易一些。

版权声明应包括署名和许可证副本。署名是你的姓名和电子邮件地址。这表明您是 VAPI 的作者,同时也是万一第三方违反许可证规定使用绑定文件时的联系人。自由软件和开放源码许可证允许复制 VAPI 文件,只要符合许可证条款即可。许可证应与库的许可证相同。这可确保绑定与程序库之间的兼容性。

版权声明应放在多行注释之间,而不是文档注释之间:

1
2
3
4
5
6
/*
 * Copyright (c) 2016 My Name <my_email@my_address.com>
 * 
 * This library is free software...[or whichever license is used by the library]
 *
 */

文档注释的开头有一个星号,即/**,会被valadoc 接收。关于valadoc的使用将在后面介绍。

CCode 属性

Vala 以特定风格生成 C 代码,例如 Vala 遵循自身的命名约定和自动生成参数的排序。CCode属性可对 Vala 生成的 C 代码进行精细控制,在使用自身约定绑定 C 库时被广泛使用。

CCode属性将用于:

  • 包括一个 C 头文件
  • 从 Vala 命名规范转换为库命名规范
  • 将库与 Vala 的辅助内存管理绑定
  • 控制函数调用参数的位置,尤其是 Vala 生成的参数
  • 克服各种边界条件

这些属性将在本教程的相关部分进行介绍。有关单个参考资料,请参阅《Vala 手册》的属性部分。

创建根命名空间

通常,一个库的所有绑定都放在一个根命名空间中。例如,libfoo 或 foolib 最好放在名为 Foo 的命名空间中。这符合上述命名约定。例如,初始 VAPI 可以是:

1
2
3
namespace Foo {
    // bindings
}

然后,可以在 Vala 程序中使用该绑定,方法是在命名空间前加上前缀,例如:

1
2
3
void main () {
    Foo.library_function();
}

或将 VAPI 命名空间纳入文件范围:

1
2
3
4
5
using Foo;

void main () {
    library_function ();
}

命名空间还提供了一种方便的函数分组方式。通常,对于基于 GLib 的库,x_y_foo模式可以直接转换为x.y.foo 命名空间。由于大多数 C 语言库并不遵循这些约定,因此情况会稍微复杂一些。作为一般经验法则,请尝试以下方法:

  • 如果全局变量、函数、常量、枚举、标志和委托定义只与类和结构体定义有关,则将它们移到类和结构体定义中。例如,将enum FooOptions移入class Foo中,使其成为简单的Options。请注意,结构体不能包含枚举、标记或委托定义,只能包含常量和静态方法。

  • 以头文件和目录为参考。如果头文件存放在foo-2.0/db/{handle,transaction,row}.hfoo-2.0/db_{handle,transaction,row}.h 中,或者foo-2.0/db.h包含了对foo_handlefoo_txfoo_row 的定义,那么创建命名空间Db很有可能是一个逻辑分组。

  • 为大组相关常量创建命名空间。有时,常量集合无法转换为枚举,在这种情况下,将它们分组到命名空间中更易于管理。

包含 C 头文件

CCode属性cheader_filename通过逗号分隔定义了要包含在生成的 C代码中的头文件列表。例如:

1
2
3
4
[CCode (cheader_filename = "libfoo/foo.h")]
namespace Foo {
    // bindings
}

尽量将头文件应用于命名空间或包含的类型。将其应用于外部上下文,可以避免在内部上下文中重复使用。

一个库通常会有一个包含多个子头文件的头文件。例如,请参见glib/glib.h头文件。在这种情况下,只需包含主头文件。

符号名称翻译

Vala 具有从 Vala 到 C 的符号名称翻译规则。默认规则遵循 GLib 命名约定,但对于绑定,名称翻译可通过lower_case_cprefixcprefixcname 的CCode属性进行自定义。

下面的示例说明了默认的符号名称翻译规则。Vala 的名称翻译规则适用于 Vala 程序和绑定。使用valac --ccode name_conversion_example.vala编译以下示例程序,然后检查 Vala 符号名称是如何翻译的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main () {
    Foo.Bar a = new Foo.Bar ();
    a.test ();
    var b = Foo.Bar.UNCHANGING;
}

namespace Foo {
    [Compact]
    class Bar {
        public const int UNCHANGING = 42;
        public void test () {
        }
    }
}

使用[Compact]属性使 C 代码更简单,因此更容易阅读,但名称翻译规则也适用于完整的 Vala 类。下面的表格总结了示例中的翻译:

Vala标识符C标识符说明
Foo.BarFooBar这是数据类型
new Foo.Bar ()foo_bar_new ()这是构造函数
a.test ()foo_bar_test (a)这是一个作用于实例的函数
Foo.Bar.UNCHANGINGFOO_BAR_UNCHANGING类型定义的常数

绑定程序库时,Vala 符号名应遵循以下约定,然后可使用lower_case_cprefixcprefixcname来确保 C 符号名与程序库相匹配:

Vala语义Vala惯例到C的默认转换通过CCode进行修改
TitleCase  
常数UPPER_SNAKE_CASE  
委托TitleCase  
枚举和标志TitleCase  
字段lower_snake_case  
方法lower_snake_case  
命名空间TitleCasetitle_case_
TITLE_CASE_
TitleCase
lower_case_cprefix
lower_case_cprefix
cprefix
属性lower_snake_case  
结构体TitleCase  
动态类型(泛型)T(单个大写字母)。对于map键和值首选KV  

在适当的情况下,将隐晦的 C 语言名称扩展为更容易理解的 Vala 语言名称(例如,将Tx扩展为Transaction)。Vala 通常比 C 语言更简洁,因此我们愿意做出不同的选择,比 C 语言程序员更倾向于可读性而非简洁性。特别是,var可以节省大量冗长类型名的编写,import 则有助于更好地使用前缀。

请注意以下几点:

  • 在名称空间中使用cprefixlower_case_cprefix

  • 在使用cprefixlower_case_cprefix时,类的优先级高于命名空间

  • 在函数和常量中使用cname

代码格式约定

  • 用于缩进的制表符
  • 括号前空格,括号后无空格
  • 等号两边有空格
  • 逗号前无空格,逗号后有空格

文档和 Valadoc.org

Valadoc.org通常是 Vala 开发人员在寻求如何使用绑定时访问的第一个网站。通过将 VAPI 添加到 Valadoc.org 上的下载软件包列表并向 Valadoc.org资源库提交拉取请求,即可将新的 VAPI 提交到Vala Extra VAPI中。请参阅libcolumbus 拉取作为示例。

Valadoc.org 经常重新生成。Valadoc.org 重新生成时,会从vala-extra-vapis中调入 VAPI 并从中生成文档。如果某个 VAPI 没有关联文档注释,Valadoc.org 将只显示 VAPI 中的符号。

在 VAPI 中的符号前添加文档注释。文档注释是带有星号的 C 语言多行注释:

1
2
3
4
5
6
7
8
9
10
/**
 * Brief description of class Foo
 *
 * Long description of class Foo, which can include an example
 */
[CCode (cname = "foo", ref_function = "foo_retain", unref_function = "foo_release")]
[Compact]
public class Foo {
    // Details of binding
}

注释可以包括附加标记。详情请参阅Valadoc 注释标记

可以在本地生成文档,以测试文档的显示效果。首先,下载并构建valadoc:

1
2
3
4
5
git clone git://git.gnome.org/valadoc
cd valadoc
./autogen.sh
make
make install

第二,生成文档:

1
2
cd my_binding_directory
valadoc --directory docs --force --package-name mybinding mybinding.vapi

这将在docs目录中生成 HTML 文档。valadoc希望docs目录不存在,但--force会覆盖这一点。--package-name mybinding将在docs中创建名为mybinding的子目录,其中包含为mybinding.vapi 生成的文档。

本地生成的文档将具有与valadoc.org 相同的结构,但视觉样式可能有所不同。

版本属性

Vala 符号可使用[Version]属性进行注释。这样就可以将一个符号标记为experimental、deprecated,并标明版本信息。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Test {
        [Version (experimental = true)]
        public void test_function_1 ();

        [Version (deprecated = true)]
        public void test_function_2 ();

        [Version (deprecated_since = "2.0")]
        public void test_function_3 ();

        [Version (deprecated = true, deprecated_since = "2.0", replacement = "test_function_5", since = "1.0")]
        public void test_function_4 ();

        [Version (since = "1.0")]
        public void test_function_5 ();
}

使用Vala的自动内存管理

在编写 Vala 代码(包括使用 C 库的代码)时,内存管理由 Vala 编译器处理。通常无需手动申请和释放内存。不过,在编写绑定代码时,准确指导 Vala 编译器如何使用 C 库的内存管理调用是整个过程的重要部分。这是一项一次性工作,意味着使用绑定的任何人都可以利用绑定的优势,更容易编写代码。

Vala 的内存分配和类型比大多数语言都要复杂。在 Python 中,所有东西都是动态类型的对象,它们都是先被分配,然后再被垃圾回收。在 C 语言中,内存分配主要由用户处理,类型只是在编译时对内存的描述。Vala 则试图同时覆盖所有这些基础。重要的是,Vala 中的类型暗示了内存管理的一些内容。

Vala 共有 4 种内存管理方案:

类型内存管理者需要处理?复制开销
值类型C编译器不需要
派生类型C编译器需要
单所有权堆分配器需要
引用计数堆分配器需要

C 语言中的指针

星号* 是 C 语言中的间接运算符。间接操作符表示标识符包含指向内存位置的指针。通常还会指明内存位置的数据类型。例如,int*identifier表示一个int保存在identifier 指向的内存位置。不过,数据类型不必指定,可以使用“通用”类型:void *identifier

可以有多个间接层次,例如char **identifier

取地址运算符是双引号,即&

间接操作符和地址操作符的使用与绑定函数签名有关,这将在后面的章节中介绍。有关 C 语言指针的全面解释,请参阅《C 语言指针须知》

目前,我们只需了解指针并不说明所指向的内存是如何管理的。从 C 代码中的指针无法得知内存是常量、栈分配还是堆分配。

C 语言中的常量、栈和堆

在 C 语言中,可以使用一种机制来分配数据,这种机制可以阻止数据在程序运行期间被更改。这些数据被称为常量。数据还可以通过另外两种方式分配:栈和堆。在编写绑定程序时,需要了解这三种方案。这主要是为了让 Vala 代码在使用绑定时能正确分配和释放堆内存,同时也是为了确保绑定不会将堆规则应用到其他两种方案中。

为了更好地理解这三种方案,我们可以分四个阶段来分析它们是如何处理内存的:

  1. 声明
  2. 分配
  3. 初始化
  4. 释放

声明告知编译器需要多少内存。例如,uint8会让编译器知道至少需要 8 位(一个字节),或者double可能比float 需要更多内存。每种类型的确切大小取决于平台,将由编译器决定。

分配是专门保留内存区域的过程。内存从哪里分配取决于内存方案。

内存分配完毕后,需要将内存初始化为所需值。例如,inta = 128;将把为int保留的内存设置为128

取消分配内存意味着程序的其他部分可以再次使用该内存。

条目声明分配初始化释放
常数编译时编译器编译时程序退出
编译时编译器运行时编译器
编译时程序员运行时程序员

C 编译器会进行一些内存管理。当项目被放置到栈或其他结构中时,编译器会创建存放这些对象所需的空间。否则,将使用mallocfree从堆中分配空间。如果任何实例包含对其他实例的引用,则需要辅助函数来分配和取消这些引用。

Vala中的所有权概念

绑定到 C 堆处理程序

Vala 的独特功能之一是同时拥有单一所有实例和引用计数实例。引用计数实例可以存储在新的位置,并通过计算引用的数量来进行内存管理;当不再有对该实例的引用时,就会销毁该实例。单独拥有的实例只有一个所有权引用,当该引用被销毁时,实例也随之销毁。因此,引用计数的对象可以通过增加引用计数来“复制”,而单个拥有的实例则无法在不复制其中实际数据的情况下进行复制(如果可能的话)。

虽然这主要是对象的问题,但 Vala 中的所有实例都必须采用其中一种内存管理方案。不同类型的对象可以遵循不同的方案,某些类型还可以根据声明中的细微差别采用不同的方案。

Vala类型分类C类型需要内存管理绑定吗?
枚举和标志int不需要
委托(has_target = false)函数指针不需要
委托(has_target = true)函数指针和Void指针不需要
委托(has_target = true)单所有权函数指针和Void指针需要,使用free_function
简单类型结构体各种基本类型或一个结构体不需要
结构体结构体,但是一结构体指针传递不需要
结构体派生类型结构体,但是一结构体指针传递需要,使用destroy_function
紧凑类单所有权指向结构体的指针需要,使用free_function
紧凑类引用计数指向结构体的指针需要,使用ref_functionunref_function
指针指向内容的指针不需要
数组单所有权元素类型的指针 (包括int类型的长度)需要,使用 free_function

识别 C 代码中的 Vala 语义

C 和 Vala 之间的一个重要区别是 Vala 在语义上更具表现力。例如,在 C 语言中char*有多种含义。它可以是一个字符串、一个数组、一个指向单个字符的指针、一个返回字符的输出参数、一个指向将被例程修改的字符的指针。这个指针是否可以为空也完全不清楚。Vala 在语法上表达了这些差异,因此编写绑定代码需要理解原始代码的意图。

最简单的方法是先查看头文件,确定所有需要绑定的重要类型。为每个类型查找分配函数、复制函数和清理函数。从中可以推断出正确的绑定策略。

常数

本小节介绍

  • C 语言中的#define预处理器指令

  • Vala编译器遵循的阶段

常量在程序运行过程中不会变化,必须是一个简单的类型或字符串。举例来说,如果 C 库通过#define语句定义了一个常量:

1
2
#define CUSTOM_PI 3.14159265358979323846

#define是预处理器的简单文本替换。因此,在编译 C 代码之前,C 预处理器会将CUSTOM_PI的相关位置替换为3.14159265358979323846。这就是没有给出类型信息的原因。此外,由于这是在编译前进行的,因此隐含了该值是常量。

在将其绑定到 Vala 时,类型信息和它是常量的信息都是明确的:

1
public const double CUSTOM_PI;

需要注意的一点是,值不绑定,只绑定标识符。Vala 会在生成的 C 代码中使用标识符,然后 C 预处理器会在编译前将其替换为值。

枚举和标志

虽然 C 语言支持枚举,但 C 程序员通常不使用枚举,而是使用 #defines。这两种结构都可以绑定到 Vala 枚举。

第一个示例是 C 枚举和 Vala 枚举之间的直接映射。C:

1
2
3
4
5
typedef enum {
    FOO_A,
    FOO_B,
    FOO_C,
} foo_e;

和Vala绑定:

1
2
3
4
5
6
[CCode (cname = "foo_e", cprefix = "FOO_", has_type_id = false)]
public enum Foo {
    A,
    B,
    C
}

请注意上例中cprefix是如何在生成 C 时将FOO_前置到所有 Vala 值中的。

第二个示例展示了如何将 C 语言中的一系列常量定义映射到 Vala 枚举中:

1
2
3
4
5
#define BAR_X 1
#define BAR_Y 2
#define BAR_Z 3


1
2
3
4
5
6
[CCode (cname = "int", cprefix = "BAR_", has_type_id = false)]
public enum Bar {
    X,
    Y,
    Z
}

检查使用枚举的地方,以确定正确的类型,不过intunsigned int是最常见的类型。

使用可组合位模式也是一种常见用法。这些可转换为 Vala 标志枚举。

1
2
3
4
#define FOO_READ (1<<0)
#define FOO_WRITE (1<<1)
#define FOO_CREATE (1<<2)

1
2
3
4
5
6
7
[CCode (cname = "int", cprefix = "FOO_", has_type_id = false)]
[Flags]
public enum Foo {
    READ,
    WRITE,
    CREATE
}

在 Vala 中,枚举和标志可以有成员函数。尤其是类似 strerr 的函数最好转换为成员函数。

枚举也可以继承,因此如果一组标志是另一组标志的超集,但它们在逻辑上是独立的,就可以使用继承来实现。

1
2
3
4
5
6
7
8
#define FOO_A 1
#define FOO_B 2
#define FOO_C 3
#define FOO_D 4
/* takes FOO_A or B only */
void do_something(int);
/* takes any FOO_ value */
void do_something_else(int);
1
2
3
4
[CCode (cname = "int", cprefix = "FOO_", has_type_id = false)]
public enum Foo { A, B }
[CCode (cname = "int", cprefix = "FOO_", has_type_id = false)]
public enum FooExtended : Foo { C, D }

简单类型结构体

C 语言库经常为数字、大小和偏移定义新类型。要将这些类型转换到 VAPI 文件中,只需在结构体中使用SimpleType属性,并从 C 头文件中的相同简单类型继承即可。

举个例子:

1
typedef uint32_t people_inside;

将在 VAPI 文件中定义为

1
2
3
4
    [SimpleType]
    [CCode (cname = "people_inside", has_type_id = false)]
    public struct PeopleInside : uint32 {
    }

从现有类型继承时,所有方法都将沿用。对于大小和偏移量,这可能是可取的;但对于句柄,这可能是不可取的。例如,UNIX 文件描述符是以整数形式存储的,但将两个文件句柄相加或相乘就没有意义了。在这种情况下,最好不要从数值类型继承,而是添加IntegerType (rank=X),这样 Vala 编译器就能在需要时自动将类型转换为适当大小的整数(例如,从整数常数初始化)。

XCB 的一个例子:

1
typedef uint32_t xcb_atom_t;

将在 VAPI 文件中定义为

1
2
3
4
5
    [SimpleType]
    [IntegerType (rank = 9)]
    [CCode (cname = "xcb_atom_t", has_type_id = false)]
    public struct AtomT {
    }

glib-2.0.vapiposix.vapi文件中定义的常用类型的级别是:

Rankglib-2.0的类型其他用户
1gint8
gfloat
 
2gchar
gdouble
 
3guchar
guint8
Posix.cc_t
4gshort
gint16
 
5gushort
guint16
 
6gint
gint32
Posixpid_t
7guint
guint32
gunichar
Posix.speed_t
Posix.tcflag_t
8glong
gssize
time_t
Posix.clock_t
9gulong
gsize
Posix.nfds_t
Posix.key_t
Posix.fsblkcnt_t
Posix.fsfilcnt_t
Posix.off_t
Posix.uid_t
Posix.gid_t
Posix.mode_t
Posix.dev_t
Posix.ino_t
Posix.nlink_t
Posix.blksize_t
Posix.blkcnt_t
10gint64 
11guint64 

结构体

请注意,C 语言侧的结构体可以包含与 Vala 等同的对象实例数据,因此可以绑定到 Vala 中的紧凑类。这将在后面的章节中介绍。本节将介绍如何将 C 结构和 C 基元绑定到 Vala 结构。

C 语言中的一种常见模式是父代结构,就像下面这样:

1
2
3
4
5
6
typedef struct {
    int a;
    int *b;
} foo_t;
void foo_init(foo_t*);
void foo_free(foo_t*);

正确的绑定方法是:

1
2
3
4
5
6
7
[CCode (cname = "foo_t", destroy_function = "foo_free", has_type_id = false)]
public struct Foo {
    int a;
    int *b; // We can do better later
    [CCode (cname = "foo_init")]
    public Foo ();
}

它的最大陷阱在于命名:foo_free并不释放传递给它的指针。除了阅读foo_free 的实现之外,我们可能无法确定这一点。对于结构体来说,必须给出Foo的完整结构。对于结构紧凑的类,它可能是不透明的(即不提供结构的内容),但也不一定。

下一个示例说明了空destroy_function的使用,并为结构体设置了默认值:

1
2
3
4
5
6
typedef struct {
    int x;
    int y;
} bar_t;
#define BAR_INITIALIZER {0, 1}

1
2
3
4
5
[CCode (cname = "bar_t", destroy_function = "", default_value = "BAR_INITIALIZER", has_type_id = false)]
public struct Bar {
    int x;
    int y;
}

值得注意的是,如果一个结构体没有指定 destroy 函数,Vala 会在结构体中存在任何看起来需要去分配的字段的情况下生成一个 destroy 函数,根据上下文的不同,其行为可能是正确的,也可能是不正确的。一个空的destroy_function将保持生成代码的正确性,并防止 Vala 生成一个析构函数。

紧凑型类

Vala 有三种类型的类:GObject 子类、GType 类和紧凑类。非基于 GLib 的 C 库的结构体可以绑定到 Vala 的紧凑类中。

单所有权类

最常见的情况是单所有权的紧凑型类,它遵循其中一种模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct foo Foo;
/* Create a new Foo handle. */
Foo *foo_make(void);
/* Make a copy of a Foo. */
Foo *foo_dup(Foo*);
/* Free a Foo handle. */
void foo_free(Foo*);

typedef struct bar *Bar;
/* Open a new Bar from a file, NULL if an error occurs. */
Bar bar_open(const char *filename);
/* Dispose of a Bar when finished. */
void bar_close(Bar);

它们都应绑定为紧凑型类。foo_makebar_open函数将分配内存,并创建一个新的类型实例(这时文档就很有用了)。这两个函数之间有一个重要的细微差别:指针在哪里被提及。在Foo 的例子中,指针在每个函数中都会被提及,而Bar则是在类型定义中就有了。Vala 总是会添加一个星号,因此Bar实际上是使用struct bar 绑定的。

第二个区别是构造函数:Foo 的构造函数不会失败,但Bar 的构造函数可能会失败。Vala 构造函数不允许返回 null。Bar 的构造函数最好绑定为静态方法,因为静态方法可以返回 null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[CCode (cname = "Foo", free_function = "foo_free")]
[Compact]
public class Foo {
    [CCode (cname = "foo_make")]
    public Foo ();

    [CCode (cname = "foo_dup")]
    public Foo dup ();
}

[CCode (cname = "struct bar", free_function = "bar_close", has_type_id = false)]
[Compact]
public class Bar {
    [CCode (cname = "bar_open")]
    public static Bar? open (string filename);
}

如果需要显式复制,可在成员函数中加入一个名为dup() 的复制函数。

引用计数类

引用计数类通常有如下模式:

1
2
3
4
typedef struct foo Foo;
Foo *foo_new();
Foo *foo_retain(Foo*);
void foo_release(Foo*);

并应绑定为:

1
2
3
4
5
6
7
8
9
10
[CCode (cname = "foo", ref_function = "foo_retain", unref_function = "foo_release")]
[Compact]
public class Foo {
    [CCode (cname = "foo_new")]
    public Foo ();
    [CCode (cname = "foo_retain")]
    public void @ref ();
    [CCode (cname = "foo_release")]
    public void unref ();
}

提供refunref函数是为了方便用户在难以满足要求情况下手动更改引用计数。

函数

这些函数可单独工作,无需事先调用其他函数即可使用。这是sync系统调用的 Posix VAPI 文件的一个简单示例:

1
2
[CCode (cname = "sync")]
void sync();

ccode 属性cname 指定了要使用的 C 名称。这样可以避免valac将当前命名空间追加到函数名称中,确保在 vala 中调用Posix.sync(),将映射到在 C 中调用sync(),而不是posix_sync()

委托

C 语言允许定义函数指针,即指向符合特定签名的代码指针,这些代码可以被执行。这样做的主要问题是,它不能通过库将信息从调用者传递到回调。在其他语言中,闭包是对代码和状态的封装。C 程序员有时会通过传递一个“用户数据”或“上下文”的 void 指针来模拟这种行为,该指针充当闭包的状态部分。

Vala 支持这两种模式:委托可以是有目标的(即闭包),也可以是无目标的(即函数指针)。这由 has_target 值控制,默认值为 true。目标位置被假定为参数列表中的最后一个值,这也是大多数 C 程序的典型位置,不过偶尔也会放在第一个。

1
2
typedef int(*compute_func)(int a, int b);
typedef double(*analyze_func)(int a, int b, void *userdata);
1
2
3
4
[CCode (cname = "compute_func", has_target = false)]
public delegate int ComputeFunc (int a, int b);
[CCode (cname = "analyze_func")]
public delegate double AnalyzeFunc (int a, int b);

如果上下文的位置不是最后一个参数,请按照更改参数位置的方法设置 CCode 属性delegate_target_pos

C 程序员通常不会为函数指针创建typedef,而是直接将其包含在内。创建一个委托,不要设置cname。如果可能,请为函数库提供一个补丁,以创建一个typedef

绑定C函数的基本原理

函数签名包括函数的参数和任何返回值。

输出和引用参数及返回值

C 语言大量使用 out 参数作为返回的替代方法。不幸的是,由于这种返回系统的非统一性,它相当令人困惑。

对于除结构体以外的所有类型,在返回时都会按照惯例返回实例。任何补充信息(委托目标、数组长度)都会作为 out 参数悄悄附加。要返回另一个值,可以将一个参数声明为out。Vala 将假定函数接受指向该值的指针,并在返回时填充该指针。ref参数与之类似,但必须在调用函数前初始化该参数,并且函数可以操作其值。

请考虑以下几点:

1
2
3
4
int div_and_mod(int a, int b, int *mod) {
    *mod = a % b;
    return a / b;
}
1
public int div_and_mod (int a, int b, out int mod);

这对类类型参数也同样适用:

1
2
3
4
5
6
int open_file_and_fd(const char *filename, FILE **file) {
    FILE *f = fopen(filename, "r");
    if (file)
    *file = f;
    return (f == NULL) ? -1 : fileno(f);
}
1
public int open_file_and_fd (string filename, out FileStream file);

对于数组和委托,这意味着同时返回参数及其相关参数:

1
void do_approximation(int *input_array, int input_length, int **output_array, int *output_length);
1
public void do_approximation (int[] input, out int[] output);

需要注意的是,当你想到“输出数组”时,实际上你想要的可能只是一个缓冲区。如果调用者分配了内存,你需要的是缓冲区,而不是输出数组。

返回结构体则截然不同。因为结构体的内存是由调用者分配的,所以结构体的 out 参数与普通指针没有区别。此外,返回结构体实际上意味着包含一个隐藏的 out 参数。

1
2
3
public struct Foo { ... }
public Foo get_foo (int x);
public void get_foo2 (int x, out Foo f);
1
2
void get_foo(int x, foo *ret);
void get_foo2(int x, foo *ret);

如果要直接返回结构体,问号操作符会将其框住,使其看起来像是堆分配的:

1
2
public Foo? get_foo (int x);
public int make_foo (int y, out Foo? f);
1
2
foo *get_foo(int x);
int make_foo(int y, foo **f);

所有权适用以下规则。

所有权

除非使用owned关键字标记,否则所有参数默认为非所有。除非使用unowned关键字标记,否则所有返回值、ref和out参数默认为所有。上述基本类型没有所有权,因为它们可以随意复制。

函数通常会返回其中一个输入值,尤其是在填充缓冲区时。所有权的正确性至关重要。如果处理不当,Vala 会获取它认为必须释放的指针的第二个副本,并释放同一块内存两次,从而导致在 Valgrind 中耗费大量时间。

如果所有权语义不正确,要么是编写了内存泄漏程序,要么是编写了双重释放程序。通常情况下,我们需要阅读源代码才能绝对确定所有权语义是正确的。

通常情况下,C程序员会将返回值标记为 const,但它们并不属于自己。

另请参见依赖类型所有权

无效性

对于大多数类型来说,添加问号允许类型为空。一般来说,C 程序员在表达特定参数是否为空方面做得很差。对于任何指针类型(数组、精简类、数组和委托),空值不会改变 C 语言的类型。也就是说,如果Foo是一个类,那么Foo fooFoo? foo具有相同的 C 语言签名。对于简单类型、枚举和标志,添加 nullability 会将类型提升为指针。也就是说,bool b的 C 语言类型是gboolean b而bool? b的 C 语言类型是gboolean *b。父结构体是一种特殊情况。当作为参数传递时,它们总是作为指针传递,因此空值性只带来语义上的区别;当作为返回值时,空值性会改变行为,这将在下文中讨论。

Vala 总是假定输出参数可以为空。例如

1
2
3
4
5
6
7
8
public delegate void ComputeFunc (int x);
public void get_compute_func (double epsilon, out ComputeFunc func);

ComputeFunc f;
get_compute_func (3.14158, out f);
f (3); // f should never be a null pointer.
get_compute_func (2.72, null); // This is perfectly okay according to Vala.

需要注意的是,空值性指的是参数的类型,而不是参数的处理方式。许多 C 语言库在访问 out 参数之前不会检查该参数是否为空,从而导致段错误。Vala 中没有导致这种情况发生的语法。

静态方法

枚举、标志、简单类型、结构体和类都可以包含函数。当 Vala 编译器生成 C 函数调用时,数据结构将作为第一个参数包含在内。为防止自动生成参数,请在 VAPI 中的函数定义中使用static关键字。

事实上,绑定静态方法比绑定成员方法更简单,因为没有实例。应注意将静态方法组织到符合逻辑的位置:有些应放在包含命名空间中,有些应放在类型定义中。一般来说,产生类型实例的方法(即像构造函数一样可能会失败的方法)属于类型定义。

更改生成参数的位置

Vala 的默认行为是保持 Vala 调用器中参数的位置与 C 函数调用者中参数的位置相同。在 Vala 端未明确参数(例如实例数据)的情况下,Vala 会假定参数位于特定位置。实例数据被假定为 C 函数的第一个参数,但可以通过instance_posCCode 属性将其更改为任何位置。Vala 位置系统可用于实例位置(instance_pos)、数组长度位置 (array_length_pos)、委托目标位置 (delegate_target_pos),甚至可用于重新排列参数(pos)。

Vala 的位置系统一开始有点令人困惑,因此需要解释一下。 假设我们有一个如下的 Vala 函数:

1
2
3
4
public class Foo {
    public delegate int Transform (double a);
    public int[] compute (int x, Transform t);
}

生成的compute签名将是

1
int *foo_compute(Foo self, int x(position = 1), foo_transform t(position = 2), void *t_userdata, int *array_len);

我在 Vala 中逐字出现的参数上标注了它们的位置。同样,t_userdata必须大于 2,array_len必须大于t_userdata,这样排序才有意义。Vala 允许用浮点数值来描述这种排序。可以把self想象成位置 0,把t 的上下文想象成位置 2.1,把返回的数组长度想象成位置 2.2。这只是一组可能的值。它也可以分别是 0.9、2.5、2.8,并产生相同的结果。

默认情况下,Vala 会将实例设置为 0,将任何数组长度设置为数组位置加 0.1,将任何委托的目标设置为委托位置加 0.1,将任何自有委托的析构函数设置为委托位置加 0.2。

如果顺序与 C 功能不符,可以使用适当的值重新排序,但必须在头脑中保持总顺序的整洁。

默认值和更改参数位置

由于 C 语言没有默认参数,因此有时会有重复的 C 语言函数以这种方式运行:

1
2
int foo_compute(Foo *f, int base_height);
int foo_compute_ex(Foo *f, int base_height, Table *t, struct opts *opts);

由于 Vala 确实有默认参数,因此只绑定扩展版本可能会有好处,但前提是默认值不太可能改变。这通常适用于文档中写明“设置为空值以自动确定”的情况。如果不确定,最好两种都绑定。

1
2
[CCode (cname = "foo_compute_ex")]
public compute (int base_height, Table t = null, opts? opts = null);

用 Vala 封装器更改签名

使用 Vala 编写的封装函数可以调整现有函数签名。这可以使签名对 Vala 更友好。

通常的做法是将 C 绑定设置为私有,并让封装器调用私有方法。封装器也写入 VAPI 文件。

变长参数(又称...

C 语言的变长参数系统非常诡谲,有很多潜在的破解方法。不幸的是,Vala 继承了它们。Vala 增加了一些安全措施,但也带来了一些新问题。

其中一个安全措施是,如果方法的CCode属性包含sentinel = "X",那么 X 将始终作为最后一个参数被追加。由于列表通常以特殊值(通常为空)结束,因此这可以防止变量参数超限。

此外,Vala 还可以通过添加PrintfFunctionScanfFunction属性,对printf-like 和scanf-like 函数进行类型检查。不过,如果格式字符串被修改为包含特殊值,这些格式标记将无法正常工作。

附加到函数末尾的返回值(如数组长度返回值和委托上下文)通常会与变量参数产生不良交互,因为 Vala 编译器会错误地将参数置于定义中的...之后。在处理变量函数时,最好明确指定所有位置。

不返回的函数

如果函数永远不会返回,NoReturn属性可以让编译器的分析器知道,在该语句之后执行的任何代码都不会被执行。这种情况很少见,但对调用abortexit的语句很有用。

更改实例引用的方法

有时,方法会返回一个指向实例的新指针(想想 realloc)。在 VAPI 中声明函数返回 void,并添加属性ReturnsModifiedPointer

1
2
typedef struct table Table;
Table *table_grow(Table *t, size_t object_count);
1
2
3
4
5
6
[Compact]
[CCode (cname = "Table")]
public class Table {
    [ReturnsModifiedPointer]
    public void grow (size_t object_count);
}

销毁实例引用的方法

如果一个方法要销毁实例(即释放实例),可以用DestroysInstance属性来标记。该方法必须返回 void。尽管在大多数情况下,这种方法会被绑定为紧凑类的free_function

如果函数会销毁一个实例,但提供了一个可用的返回值,则应将其绑定为一个静态方法,该方法会获取一个实例的自有变量:

1
2
3
4
5
typedef struct transaction Transaction;
Transaction begin_tx(Database *db);
void transaction_abort(Transaction *tx);
void transaction_commit(Transaction *tx);
bool transaction_try_commit(Transaction *tx);
1
2
3
4
5
6
7
8
[Compact]
[CCode (cname = "Transaction", free_function = "transaction_abort")]
public class Transaction {
    public Transaction (Database db);
    [DestroysInstance]
    public void commit ();
    public static bool try_commit (owned Transaction tx);
}

添加Vala友好语义

所有绑定的方法都应该是公共的,除非是在某些尴尬的情况下。Vala 编译器不尊重 VAPI 文件中的可见性,因此定义私有方法只是防止它们出现在Valadoc 中,而不是防止它们被访问。

Vala 有一些特殊的方法名称,允许使用 Vala 语法。可以使用CCode属性捕捉 C 和 Vala 之间的差异。

to_string () 方法

将方法绑定为to_string ()将允许在 Vala 字符串模板中使用该方法,而无需在 Vala 源代码中写入方法标识符。

属性

Vala 允许紧凑型类具有属性,这些属性是 getset 方法对的语法糖。通常,具有不透明实现的 C 对象会提供一系列函数来查询实例的状态。这些函数可以通过以下方式转换为属性:

  • get方法的签名是T get(I self)set方法的签名是void set(I self, T val)。它们实际上不必成对出现。

  • get方法不会产生用户无法察觉的副作用。

  • get方法的调用成本很低。

  • set方法不会返回错误信息。

与大多数返回类型不同,除非明确标示为owned,否则get方法的返回被假定为非拥有。

考虑一下

1
2
3
4
typedef struct foo Foo;
int foo_item_count(Foo f);
int foo_max_items(Foo f);
void foo_set_max_items(Foo f);
1
2
3
4
5
6
7
8
9
public class Foo {
    public int item_count {
        [CCode (cname = "foo_item_count")] get;
    }
    public int max_items {
        [CCode (cname = "foo_max_items")] get;
        [CCode (cname = "foo_set_max_items")] set;
    }
}

所有常见的CCode属性都可以应用于get;set;,而owned属性则可以应用于更改get; 的默认所有权。请注意,改变属性的所有权是错误的,除非实例实际上并不拥有set; 提供给它的值。

集合

Vala 有几个标准方法名,这些方法名是为与 Vala 语法(如foreach)配合使用而设计的。

Vala 使用get ()方法实现方括号索引语法。例如,一个list实例的get方法返回一个list项,即list.get (index),也可以写成list[index]

在下一个示例中,C 函数签名返回集合中的一个项目:

1
2
3
blkid_partition
blkid_partlist_get_partition (blkid_partlist ls,
                              int n);

这可以在 VAPI 中绑定为:

1
2
3
4
5
6
[Compact]
[CCode (cname = "blkid_partlist")]
public class ListOfPartitions {
    [CCode (cname = "blkid_partlist_get_partition")]
    public unowned Partition get (int index);
}

请注意,[CCode (cname = "blkid_partlist_get_partition")]用于将 Vala 方法名称get更改为 C 语言所需的名称:

1
var partition = partitions [count];

set是Vala 用来替换集合中某一项目的方法。set必须返回void

getset这两种索引方法可以和 C 函数一样接受多个参数,从而可以绑定多维索引。在 Vala 中使用set时,最后一个参数必须是新值。

通过将 Vala 中的size属性绑定到用 C 语言返回集合大小的函数上,就可以在集合中使用 Vala 的foreach关键字。同时还需要使用获取索引方法。下面的示例延续了上面的 PartitionList 示例。获取列表大小的 C 函数签名是:

1
2
int
blkid_partlist_numof_partitions (blkid_partlist ls);

这将绑定为:

1
2
3
4
5
6
7
[Compact]
[CCode (cname = "blkid_partlist")]
public class ListOfPartitions {
    [CCode (cname = "blkid_partlist_get_partition")]
    public unowned Partition get (int index);
    public int size { [CCode (cname = "blkid_partlist_numof_partitions")] get; }
}

请注意,CCode的cname译名位于属性主体内部。

现在可以在 Vala 代码中使用foreach 绑定:

1
foreach (var partition in partitions) { /* do something with the partition */ }

如果集合为非所有,则 Vala 会给出错误信息: duplicating ListOfPartitions instance, use unowned variable or explicitly invoke copy method.(正在复制 ListOfPartitions 实例,请使用非所有变量或显式调用复制方法。) 请参见Bug 661876

对于非自有集合,for循环仍然有效:

1
2
3
4
for (int count = 0; count < partitions.size; count++) {
    var partition = partitions [count];
    /* do something with the partition */
}

对于容器实例,Vala 提供了语法糖,可将某些操作转换为方法调用:

1
2
3
4
5
x in a -> a.contains (x)
a[x, y] -> a.get (x, y)
a[x, y] = z -> a.set (x, y, z);
foreach (var x in a) { ... } -> var x; var i = a.iterator (); while ((x = i.next_value ()) != null) {...}
foreach (var x in a) { ... } -> var i = a.iterator (); while (i.next ()) { var x = i.get (); ... }

如果合适,提供与这些原型相匹配的方法将允许使用这些语法糖。

contains必须返回bool

迭代器需要一个中间对象来保持迭代状态。该类必须实现一个 next_value 函数,返回下一个值,如果要停止迭代,则返回空值;或者它可以有一个next方法,其签名为bool next (),用于移动到下一个元素,如果有,则返回 true;还可以有一个T get ()方法,用于获取迭代器的当前值。C 程序很少有这样的接口。

请根据自己的判断来决定是否使用这些约定。这虽然是对接口的修改,但确实会使生成的接口更易于使用。

绑定 C 函数的参数和返回类型

基本类型

最基本的类型(intdoublesize_t)可以简单地翻译。有些类型有不同的版本(例如,uint32_tu_int32_t是相同的,但在不同的头文件中定义),但在绑定时都可以统一。

结构体

大多数库都接收通过引用传递的结构体,Vala 的默认行为也是通过引用传递结构体。因此,要在函数或方法调用中将结构体作为参数传递,只需指定结构类型和变量名称即可。例如 C 代码

1
2
3
4
5
typedef struct foo {
    int x;
    int y;
};
void compute_foo(foo *f);

将被绑定为:

1
2
3
4
5
6
[CCode (cname = "foo", has_type_id = false)]
public struct Foo {
    public int x;
    public int y;
};
void compute_foo(Foo f);

很少有 C 库函数是为了接收通过值而非引用传递的结构体而编写的。您会在 C 函数的参数中看到struct关键字。您还可能看到const struct。为了让 Vala 按值传递结构体,需要在结构体的 Vala 绑定中添加[SimpleType]注解。下面是 C 语言中的模式:

1
2
3
4
5
typedef struct foo {
    int x;
    int y;
};
void compute_foo(struct foo f);

将被绑定为:

1
2
3
4
5
6
7
[CCode (cname = "foo", has_type_id = false)]
[SimpleType]
public struct Foo {
    public int x;
    public int y;
}
void compute_foo(Foo f);

数组

Vala 数组的设计符合大多数 C 数组语义。由于 C 数组通常没有明确的长度,Vala 需要特殊的提示才能知道该怎么做。关于数组的长度,有以下几种情况。对于参数,附加在该参数上的CCode属性控制数组的绑定。对于返回值,方法的 CCode属性控制数组的绑定。

数组长度作为参数传递

默认情况下,Vala 假设是第一种情况,并进行以下转换:

1
2
void foo (double[] array);
double[] foo (float f);
1
2
void foo(double *array, int array_length);
double *foo(float f, int *array_length);

如果 C 代码这样做,仍有两个潜在的不匹配:参数的顺序和数组长度的类型。通常情况下,数组长度是size_tunsigned intarray_length_pos可以移动数组长度参数的位置,参见改变参数生成位置array_length_type指定一个字符串,表示数组的 C 类型(例如size_t)。

数组为空终止

array_null_terminated将假定数组像字符串一样以空值结束,并通过遍历数组中的项目自动设置数组长度。由于 Vala 总是以最后一个元素为空在数组中分配填充,因此传递一个 Vala 声明的数组并不涉及以任何方式修改数组。

数组长度是一个常量表达式

array_length_cexpr可以设置为填充数组值的 C 表达式。它不能访问数组、被调用对象的实例或任何其他上下文。它必须是一个无上下文的表达式。

数组长度未知

如果数组长度未知,在CCode属性中设置array_length = false将导致 Vala 将数组的.length属性设置为-1,并在作为参数使用时不传递长度。

通过一些笨拙的方法了解数组长度

这只适用于返回的数组。如果数组的长度可以确定,但不是绝对的,则可以包含一个封装函数,将数组的.length 属性设置为正确的值。请参阅数组长度

字符串和缓冲区

在 C 语言中,字符串和缓冲区通常被当作数组处理,但在 Vala 中可能需要稍微精细一些。在 Vala 中,字符串是UTF-8 数据的空端列表,不可更改。如果使用情况并非如此,则处理该数据的首选方式是uint8数组。

函数经常使用一个缓冲区,将字符串填充其中,然后返回缓冲区或空值(例如realpath(3))。通常情况下,缓冲区应为uint8[],返回值应为unowned string?

再次,彻底检查返回字符串的所有权。通常情况下,调用者不会释放字符串,尤其是在标记为常量的情况下。

函数指针

C 语言中的函数指针在 Vala 中绑定为委托。委托是一种声明函数指针应具有的函数签名的类型。函数指针还可以有一个相关的数据参数,称为目标。

对于没有目标的委托,可以简单地将其视为简单类型。

对于目标委托,必须包含目标。默认情况下,Vala 假定目标位置在函数指针本身之后,但可以通过delegate_target_pos 进行调整。接收目标的位置是在委托的定义中定义的,而不是在调用函数中定义的。

具有目标的委托不能被简单复制,因为目标也必须被复制。因此,目标委托的处理方式很像单个拥有的类,它们可以被重新分配,但不能被多重引用。

如果方法要保留对委托的引用,那么它需要一个辅助函数来在委托完成后销毁委托。该位置位于目标之后,但可以通过delegate_target_destroy_notify_pos 设置。

如果返回的是委托(这种情况比较少见),则目标和销毁通知符被假定为 out 参数。

1
2
3
4
5
6
typedef void (*foo_func)(int x, void *context);

void call_foo(foo_func f, void *context);
void call_foo_later(foo_func f, void *context, void(*free_context)(void*));
foo_func get_foo(void **context);
foo_func make_foo(void **context, void(**free_context)(void*));
1
2
3
4
5
6
7
[CCode (cname = "foo_func", has_target = true)]
public delegate void FooFunc (int x);

public void call_foo (FooFunc f);
public void call_foo_later (owned FooFunc f);
public unowned FooFunc get_foo ();
public FooFunc make_foo ();

变量类型参数(泛型)

Vala 的泛型可以应用于使用 void 指针作为泛型值参数的 C 函数。内存管理和泛型往往相处得不好,因此尽可能避免这种情况可能会有好处。特别是,拥有泛型实例的泛型结构体可能会表现奇怪。此外,将拥有的结构体放入泛型集合中也会导致崩溃。

在开始之前,请确定类型变量的作用域:它适用于方法还是类?泛型与委托配对。按如下方式绑定委托:

1
typedef int (*foo_func)(void *a, void *b, void* context);
1
2
[CCode (cname = "foo_func", simple_generics = true)]
public delegate int FooFunc<T> (T a, T b);

泛型方法

通常,泛型变量的上下文是一个方法。只需将simple_generics应用于CCode属性即可:

1
void sort(void **array,int array_length,foo_func compare,void*context);
1
2
[CCode(simple_generics=true)]
public void sort<T>(T[] array, FooFunc<T> compare);

有时,这不是一个 C 函数,而是一个使用类型名称(如va_arg)的类函数宏,在这种情况下,将generic_type_pos设为参数的位置:

1
2
#define sort(array, type, compare, context) ...

1
2
[CCode(generic_type_pos=1.1)]
public void sort<T>(T[] array,FooFunc<T> compare);

泛型类和结构体

如果数据结构类似于容器,那么就有可能使用泛型来绑定结构。然而,Vala 对泛型结构的假设相当僵化,因此这可能是不可能的。

  • 在类上创建一个类型变量。
  • simple_generics 装饰所有使用类型变量的方法。
  • 如果提供了simple_generics,类的构造函数应将析构函数作为参数。如果构造函数不带参数,则使用simple_generics将所有构造函数转换为静态方法。

  • 验证所有的所有权。当 Vala 输出自有变量的 simple_generics代码时,总是会传递析构函数。在编写 C 程序时,经常会在构造函数中传递一次析构函数。在这种情况下,应将析构函数设置为空,并坚持所有值都不属于自己。

用户指针情形

通常情况下,C 语言程序库会为一些与对象相关的用户数据设置一个指针,而这些数据完全由用户掌握。这很容易绑定。

1
2
3
typedef struct foo Foo;
void *foo_get_userptr(Foo*);
void foo_set_userptr(Foo*,void*);
1
2
3
4
5
6
public class Foo<T> {
    public unowned T? user_data {
        [CCode (cname = "foo_get_userptr", simple_generics = true)] get;
        [CCode (cname = "foo_set_userptr", simple_generics = true)] set;
    }
}

唯一需要注意的是,这种绑定方式很有感染力:在其他上下文中使用Foo的所有方法,包括该对象的数组和包含该类型的其他类,都必须应用simple_generics属性。为了避免这种情况,另一种绑定方法是:

1
2
3
4
5
6
public class Foo {
   [CCode (simple_generics = true)]
   public void set_user_ptr<T> (T value);
   [CCode (simple_generics = true)]
   public T get_user_ptr<T> ();
}

不过,这种方案的类型安全性较低。

指针

如果你已经做了这么多,但这个东西似乎仍然需要是一个指针,那么它就是一个指针,但这是一个耻辱的徽章。

绑定 C 结构体的字段

紧凑型类、结构体和简单类型结构体可能有字段。通常情况下,类是不透明的;也就是说,类的内容没有任何信息。如果是这样,请跳过本节。绑定字段时,首先要检查是否存在同名的 getter/setter 函数(请参阅 “属性“)。通常情况下,结构的细节都在头文件中,但并不打算公开;请避免绑定不应被访问的变量。请查阅文档。

结构体

任何简单类型(int、double、enum或同一绑定中的简单类型)都可以通过在前面加上public 来绑定。这也适用于任何非直接指向的父代结构体(即它们是foo f;,而不是foo *f;)。

结构体指针

作为指针引用的任何字段都稍显复杂。

如果类型是父结构或字段可能为空,则在类型后添加问号。

1
foo_t *myfoo;
1
public foo? myfoo;

接下来的问题是:该引用是否被拥有?如果值被覆盖,是否应该调用析构函数?如果答案是否定的,那么就使用unowned 作为前缀。对于有父指针的树形结构来说,情况往往如此。

1
foo_t *parent;
1
public unowned Foo parent;

如果缺少 unowned,则在字段被覆盖时会发生重复释放事件。如果在不需要时将其包含在内,则会出现内存泄漏。

数组

在 C 语言中,数组有两种类型:分配内存的指针或包含在结构的指针。Vala 也遵循类似的约定:

1
2
int foo[20];
int *bar;
1
2
public int foo[20];
public int[] bar;

请注意 Vala 版本中方括号的位置。对于固定长度的数组,Vala 希望方括号(以及长度)跟在变量名后面,而对于动态大小的数组,Vala 希望方括号跟在类型后面(不包含长度)。

同样,如果数组可能为空,则在类型后加上问号。

Vala 数组有与之相关的长度。通常,C 程序员也会这样做:

1
2
int *foo;
size_t foo_count;

其绑定方式为:

1
2
[CCode(array_length_cname="foo_count",array_length_type="size_t")]
public int[] foo;

通常情况下,数组将以空值结束,因此不会包含大小:

1
2
[CCode(array_null_terminated=true)]
public Foo[] foos;

有时,长度不包括在内,而是在其他地方定义,如常数:

1
2
[CCode(array_length_cexpr="FOO_COUNT")]
public Foo[] foos;

由于 Vala 只允许使用数值作为数组长度,因此如果数组长度会随库的新版本而变化,使用array_length_cexpr可能会比较方便。

Vala 并不能真正实现 C 风格的堆叠数组(又称锯齿状多维数组),因此,如果没有额外的 C 代码,将它们绑定为数组几乎是不可能的。由于 Vala 的指针语义相同,因此可以将它们视为指针。

函数指针

函数指针字段的复杂程度取决于所有权和目标。如果委托是无目标的,那么它就可以被视为一个简单类型,不需要考虑所有权问题。

如果委托有一个目标,那么 C 结构必须有一个目标的持有者:

1
2
3
4
5
6
typedef void(*foo_func)(int a, void *userdata);

typedef struct {
    foo_func callback;
    void *callback_context;
} foo;
1
2
3
4
5
6
7
[CCode (cname = "foo_func")]
public delegate void FooFunc(int a);

public struct Foo {
    [CCode (delegate_target_cname = "callback_context")]
    public unowned FooFunc callback;
}

按照惯例检查无效性。

所有权稍微复杂一些,因为必须有一个字段来保存释放上下文数据的函数。用 GLib 术语来说,这就是销毁通知。

1
2
3
4
5
6
7
typedef void(*foo_func)(int a, void *userdata);

typedef struct {
    foo_func callback;
    void *callback_context;
    void(*callback_free)(void*);
} foo;
1
2
3
4
5
6
7
[CCode (cname = "foo_func")]
public delegate void FooFunc(int a);

public struct Foo {
    [CCode (delegate_target_cname = "callback_context", delegate_target_destroy_notify_cname = "callback_free")]
    public FooFunc callback;
}

如果函数指针将被精确调用一次,并且调用它将导致上下文销毁,则使用scope = "async"

1
2
3
typedef void(*start_job)(int priority, void *context);

void threadpool_queue_job(Pool *p, start_job j, void *context);
1
2
3
4
5
6
[CCode (scope = "async", cname = "start_job")]
public delegate void StartJob (int priority);

public class ThreadPool {
        public void queue_job (StartJob j);
}

共用体

Vala 不理解共用体,但共用体中的名称可以作为 cname 的一部分。

1
2
3
4
5
6
7
typedef struct {
    bool which_one;
    union {
        double d;
        int i;
    } data;
} foo_t;
1
2
3
4
5
6
7
public struct Foo {
    public bool which_one;
    [CCode (cname = "data.d")]
    public double data_d;
    [CCode (cname = "data.i")]
    public int data_i;
}

额外提示

你可以多次绑定一个方法。特别是,使用父对象的构造函数通常既可以作为子对象的构造函数,也可以作为父对象实例的方法。

有时,类的cname ="void"可以绕过糟糕的类型定义,但绝对不能用于委托,因为将 void 指针转换为函数指针不符合 C 语言的规定。

如果能在类定义中添加有用的方法,可以让类更像 Vala。

尴尬局面

有一些尴尬的情况经常出现,需要得到解答。

数组长度

有时,带有返回数组的函数会包含长度,但与 Vala 期望的方式不同。最常见的两种情况是:

1
2
3
4
5
6
7
int get_array(foo**out_array_p);

struct {
foo *data;
int size;
} array_with_length;
void get_data(array_with_length *output);

可以绑定为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[CCode (cname = "get_array")]
private int _get_array ([CCode (array_length = false)] out foo[] a);
[CCode (cname = "vala_get_array")]
public foo[] get_array () {
    foo[] temp;
    var len = _get_array (out temp);
    temp.length = len;
    return (owned) temp;
}

[CCode (cname = "array_with_length", destroy_function = "")]
private struct array_with_length {
    [CCode (array_length_name = "size")]
    foo[] data;
}
[CCode (cname = "get_data")]
private void _get_data (out array_with_length a);
[CCode (cname = "vala_get_data")]
public foo[] get_data () {
    array_with_length temp;
    _get_data (out temp);
    return (owned) a.data;
}

独立类型所有权

函数可以有条件地获取对象的所有权。这取决于参数或返回值。在有参数的情况下,函数可以这样绑定:

1
void somefunc(foo *data,bool free_when_done);
1
2
3
4
5
6
7
8
9
10
11
12
13
[CCode (cname = "somefunc")]
private _somefunc(Foo data, bool free_when_done);
[CCode (cname = "")]
private _sink_foo (owned Foo foo);
[CCode (cname = "vala_somefunc")]
public somefunc (Foo data) {
    _somefunc(data, false);
}
[CCode (cname = "vala_somefunc_owned")]
public somefunc_owned (owned Foo data) {
    _somefunc (data, true);
    _sink_foo ((owned) foo);
}

当返回代码是依赖类型的来源时,这种情况就比较尴尬。一种选择如下:

1
2
/* foo is freed if return value is 3. */
int awkward(foo*);
1
2
3
4
5
6
7
8
9
10
11
[CCode (cname = "")]
private void _sink_foo (owned Foo f);
[CCode (cname = "awkward")]
private int _awkward (Foo f);
[CCode (cname = "vala_awkward")]
public int awkward (ref Foo f) {
    var ret = _awkward (f);
    if (ret == 3)
        _sink_foo ((owned)f);
    return ret;
}

成员长度

在处理原始内存访问时,有一种常见的模式:

1
void foo(void *data, size_t size, size_t nmemb);

在这种情况下,通常最好将数据类型固定为uint8,并使用合适的大小作为默认参数:

1
public void foo([CCode (array_length_pos = 2.1)] uint8[] data, size_t size = 1);

无主对象的有主数组

Vala 没有方便的方法来表达由非自有对象组成的自有数组。参见bug 571486

共享上下文委托

当传递多个委托时,它们有时会共享一个上下文指针:

1
void foo(void *context, void(*x)(int a, void *context), void(*y)(double a, void *context));

在这里,x和y共享上下文,但 Vala 没有办法表达这一点。不过,有一个解决方法:

1
2
3
4
5
6
[CCode (simple_generics = true, has_target = false)]
public void X<T> (int a, T context);
[CCode (simple_generics = true, has_target = false)]
public void Y<T> (double a, T context);
[CCode (simple_generics = true);]
public void foo<T> (T context, X<T> x, Y<T> y);

这样就不容易传递 lambda,但传递类或结构体却很实用。

本文由作者按照 CC BY 4.0 进行授权