仓颉调用 C 语言详解​

仓颉调用 C 语言详解​

仓颉调用 C 语言详解 ​在仓颉编程语言中,可以通过 FFI (Foreign Function Interface) 机制调用 C 语言编写的函数和库。这对于复用现有的 C 代码库、调用系统 API 或者实现高性能的关键代码非常有用。

基础概念 ​仓颉与 C 类型映射关系 ​在调用 C 函数时,了解仓颉类型与 C 类型的对应关系非常重要。以下是主要的类型映射表:

Cangjie TypeC TypeSize (byte)Unitvoid0Boolbool1UInt8char1Int8int8_t1UInt8uint8_t1Int16int16_t2UInt16uint16_t2Int32int32_t4UInt32uint32_t4Int64int64_t8UInt64uint64_t8IntNativessize_tplatform dependentUIntNativesize_tplatform dependentFloat32float4Float64double8CStringchar*platform dependentCPointervoid*platform dependentCPointerT*platform dependenttype Callback = CFunc<(Int32) -> Unit>typedef void (*Callback)(int)platform dependent注意:

long long 是 C99 标准的 64 位整数类型long 在某些 64 位平台上的 64 位整数类型,但存在平台差异,推荐优先使用 int64_t 保证跨平台一致性在定义与 C 交互的结构体时,需要使用 @C 注解标记。

编写 C,编译成 lib ​首先我们需要编写 C 代码并编译成共享库(在 Windows 上是 DLL)。

htypedef struct {

int64_t x;

int64_t y;

} Point;

typedef struct {

float x;

float y;

float z;

} Cube;

extern "C" {

__declspec(dllexport) int32_t drawPicture(Point* point, Cube* cube);

}1234567891011121314cpp#include

#include

#include "demo.h"

int32_t drawPicture(Point* point, Cube* cube) {

point->x = 1;

point->y = 2;

printf("Draw Point finished.\n");

printf("Before draw cube\n");

printf("%f\n", cube->x);

printf("%f\n", cube->y);

printf("%f\n", cube->z);

cube->x = 4.4;

cube->y = 5.5;

cube->z = 6.6;

printf("Draw Cube finished.\n");

return 0;

}12345678910111213141516171819对应的 CMakeLists.txt 文件:

cmakecmake_minimum_required(VERSION 3.28)

project(demo)

set(CMAKE_CXX_STANDARD 17)

# add_executable(demo main.cpp)

# 指定输出的 DLL 名称

set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE) # 使得所有符号被导出

add_library(demo SHARED main.cpp) # 创建一个共享库(DLL)12345678910查看导出符号 ​编译完成后会生成 libdemo.dll,需要将其复制到仓颉工程目录。

在 Windows 下可使用 VS Powershell 的 dumpbin /EXPORTS .\libdemo.dll 查看导出的符号是否正常。

Dump of file .\libdemo.dll

File Type: DLL

Section contains the following exports for libdemo.dll

00000000 characteristics

674C3898 time date stamp Sun Dec 1 18:21:12 2024

0.00 version

1 ordinal base

2 number of functions

2 number of names

ordinal hint RVA name

1 0 00008DD0 _Z6printfPKcz

2 1 000014B0 drawPicture1234567891011121314151617可以看到我们定义的 drawPicture 函数已经被正确导出。

基本使用方法 ​编写仓颉程序 ​在仓颉程序中,我们需要定义与 C 结构体对应的结构体,并声明外部函数。

cjpackage call_c

@C

struct Point {

var x: Int64 = 0

var y: Int64 = 0

}

@C

struct Cube {

var x: Float32 = 0.0

var y: Float32 = 0.0

var z: Float32 = 0.0

init(x: Float32, y: Float32, z: Float32) {

this.x = x

this.y = y

this.z = z

}

}

// 声明外部 C 函数

foreign func drawPicture(point: CPointer, cube: CPointer): Int32

main() {

// 分配内存空间

let pPoint = unsafe { LibC.malloc() }

let pCube = unsafe { LibC.malloc() }

// 初始化 Cube 对象

var cube = Cube(1.1, 2.2, 3.3)

unsafe {

// 将 cube 写入分配的内存

pCube.write(cube)

// 调用 C 函数

drawPicture(pPoint, pCube) // 在其中 x, y 的值会被改变

// 输出结果

println(pPoint.read().x)

println(pPoint.read().y)

println(pCube.read().x)

println(pCube.read().y)

println(pCube.read().z)

// 释放内存

LibC.free(pPoint)

LibC.free(pCube)

}

}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051C 调用仓颉的函数 ​除了仓颉调用 C 函数之外,仓颉也支持让 C 语言调用仓颉函数。这种双向互操作性对于集成现有 C 代码库或将仓颉代码嵌入到 C 项目中非常有用。

CFunc 类型 ​仓颉提供 CFunc 类型来对应 C 侧的函数指针类型。C 侧的函数指针可以传递到仓颉,仓颉也可以构造出对应 C 的函数指针的变量传递到 C 侧。

假设一个 C 的库 API 如下:

ctypedef void (*callback)(int);

void set_callback(callback cb);12对应的,在仓颉里面这个函数可以声明为:

cangjieforeign func set_callback(cb: CFunc<(Int32) -> Unit>): Unit1构造 CFunc 类型的方法 ​CFunc 类型的变量可以从 C 侧传递过来,也可以在仓颉侧构造出来。在仓颉侧构造 CFunc 类型有两种办法:

用 @C 修饰的函数标记为 CFunc 类型的闭包@C 修饰的函数,表明它的函数签名是满足 C 的调用规则的,定义还是写在仓颉这边。foreign 修饰的函数定义是在 C 侧的。

注意事项 ​注意:

foreign 修饰的函数与 @C 修饰的函数,这两种 CFunc 的命名不建议使用 CJ_(不区分大小写)作为前缀,否则可能与标准库及运行时等编译器内部符号出现冲突,导致未定义行为。

完整示例 ​cangjie// 使用 @C 注解定义符合 C 调用规范的函数

@C

func myCallback(s: Int32): Unit {

println("handle ${s} in callback")

}

main() {

// 参数是用 `@C` 修饰的函数

unsafe { set_callback(myCallback) }

// 参数是标记为 `CFunc` 类型的 lambda 表达式

let f: CFunc<(Int32) -> Unit> = { i => println("handle ${i} in callback") }

unsafe { set_callback(f) }

}1234567891011121314栈保护要求 ​在编译与仓颉互操作的 C 代码时,请打开 -fstack-protector-all/-fstack-protector-strong 栈保护选项,仓颉侧代码默认拥有溢出检查与栈保护功能。在引入 C 代码后,需要同步保证 unsafe 块中的溢出的安全性。

高级特性 ​inout 参数 ​在仓颉中调用 CFunc 时,其实参可以使用 inout 关键字修饰,组成引用传值表达式,此时,该参数按引用传递。引用传值表达式的类型为 CPointer,其中 T 为 inout 修饰的表达式的类型。

引用传值表达式具有以下约束:

仅可用于对 CFunc 的调用处。其修饰对象的类型必须满足 CType 约束,但不可以是 CString。其修饰对象不可以是用 let 定义的,不可以是字面量、入参、其他表达式的值等临时变量。通过仓颉侧引用传值表达式传递到 C 侧的指针,仅保证在函数调用期间有效,即此种场景下 C 侧不应该保存指针以留作后用。inout 修饰的变量,可以是定义在顶层作用域中的变量、局部变量、struct 中的成员变量,但不能直接或间接来源于 class 的实例成员变量。

下面是一个例子:

cangjieforeign func foo1(ptr: CPointer): Unit

@C

func foo2(ptr: CPointer): Unit {

let n = unsafe { ptr.read() }

println("*ptr = ${n}")

}

let foo3: CFunc<(CPointer) -> Unit> = { ptr =>

let n = unsafe { ptr.read() }

println("*ptr = ${n}")

}

struct Data {

var n: Int32 = 0

}

class A {

var data = Data()

}

main() {

var n: Int32 = 0

unsafe {

foo1(inout n) // OK

foo2(inout n) // OK

foo3(inout n) // OK

}

var data = Data()

var a = A()

unsafe {

foo1(inout data.n) // OK

foo1(inout a.data.n) // Error, n is derived indirectly from instance member variables of class A

}

}1234567891011121314151617181920212223242526272829303132333435注意:

使用宏扩展特性时,在宏的定义中,暂时不能使用 inout 参数特性。

调用约定 ​函数调用约定描述调用者和被调用者双方如何进行函数调用(如参数如何传递、栈由谁清理等),函数调用和被调用双方必须使用相同的调用约定才能正常运行。仓颉编程语言通过 @CallingConv 来表示各种调用约定,支持的调用约定如下:

CDECL:CDECL 表示 clang 的 C 编译器在不同平台上默认使用的调用约定。STDCALL:STDCALL 表示 Win32 API 使用的调用约定。通过 C 语言互操作机制调用的 C 函数,未指定调用约定时将采用默认的 CDECL 调用约定。如下调用 C 标准库函数 rand 示例:

cangjie@CallingConv[CDECL] // Can be omitted in default.

foreign func rand(): Int32

main() {

println(unsafe { rand() })

}123456@CallingConv 只能用于修饰 foreign 块、单个 foreign 函数和顶层作用域中的 CFunc 函数。当 @CallingConv 修饰 foreign 块时,会为 foreign 块中的每个函数分别加上相同的 @CallingConv 修饰。

数组 ​仓颉使用 VArray 类型与 C 的数组类型映射,VArray 可以作为函数参数和 @C struct 成员。当 VArray 中的元素类型 T 满足 CType 约束时, VArray 类型也满足 CType 约束。

作为函数参数类型:

当 VArray 作为 CFunc 的参数时, CFunc 的函数签名仅可以是 CPointer 类型或 VArray 类型。当函数签名中的参数类型为 VArray 时,传递的参数仍以 CPointer 形式传递。

VArray 作为参数的使用示例如下:

cangjieforeign func cfoo1(a: CPointer): Unit

foreign func cfoo2(a: VArray): Unit12对应的 C 侧函数定义可以是:

cvoid cfoo1(int *a) { ... }

void cfoo2(int a[3]) { ... }12调用 CFunc 时,需要通过 inout 修饰 VArray 类型变量:

cangjievar a: VArray = [1, 2, 3]

unsafe {

cfoo1(inout a)

cfoo2(inout a)

}12345VArray 不允许作为 CFunc 的返回值类型。

作为 @C struct 成员:

当 VArray 作为 @C struct 成员时,它的内存布局与 C 侧的结构体排布一致,需要保证仓颉侧声明长度与类型也与 C 完全一致:

cstruct S {

int a[2];

int b[0];

}1234在仓颉中,可以声明为如下结构体与 C 代码对应:

cangjie@C

struct S {

var a = VArray(repeat: 0)

var b = VArray(repeat: 0)

}12345注意:

C 语言中允许结构体的最后一个字段为未指明长度的数组类型,该数组被称为柔性数组(flexible array),仓颉不支持包含柔性数组的结构体的映射。

字符串 ​特别地,对于 C 语言中的字符串类型,仓颉中设计了一个 CString 类型来对应。为简化为 C 语言字符串的操作,CString 提供了以下成员函数:

init(p: CPointer) 通过 CPointer 构造一个 CStringfunc getChars() 获取字符串的地址,类型为 CPointerfunc size(): Int64 计算该字符串的长度func isEmpty(): Bool 判断该字符串的长度是否为 0,如果字符串的指针为空返回 truefunc isNotEmpty(): Bool 判断该字符串的长度是否不为 0,如果字符串的指针为空返回 falsefunc isNull(): Bool 判断该字符串的指针是否为 nullfunc startsWith(str: CString): Bool 判断该字符串是否以 str 开头func endsWith(str: CString): Bool 判断该字符串是否以 str 结尾func equals(rhs: CString): Bool 判断该字符串是否与 rhs 相等func equalsLower(rhs: CString): Bool 判断该字符串是否与 rhs 相等,忽略大小写func subCString(start: UInt64): CString 从 start 开始截取子串,返回的子串存储在新分配的空间中func subCString(start: UInt64, len: UInt64): CString 从 start 开始截取长度为 len 的子串,返回的子串存储在新分配的空间中func compare(str: CString): Int32 该字符串与 str 比较,返回结果与 C 语言的 strcmp(this, str) 一样func toString(): String 用该字符串构造一个新的 String 对象func asResource(): CStringResource 获取 CString 的 Resource 类型另外,将 String 类型转换为 CString 类型,可以通过调用 LibC 中的 mallocCString 接口,使用完成后需要对 CString 进行释放。

CString 的使用示例如下:

cangjieforeign func strlen(s: CString): UIntNative

main() {

var s1 = unsafe { LibC.mallocCString("hello") }

var s2 = unsafe { LibC.mallocCString("world") }

let t1: Int64 = s1.size()

let t2: Bool = s2.isEmpty()

let t3: Bool = s1.equals(s2)

let t4: Bool = s1.startsWith(s2)

let t5: Int32 = s1.compare(s2)

let length = unsafe { strlen(s1) }

unsafe {

LibC.free(s1)

LibC.free(s2)

}

}12345678910111213141516171819CFunc 和 unsafe ​CFunc 的多种形式 ​仓颉中的 CFunc 指可以被 C 语言代码调用的函数,共有以下三种形式:

@C 修饰的 foreign 函数@C 修饰的仓颉函数类型为 CFunc 的 lambda 表达式,与普通的 lambda 表达式不同,CFunc lambda 不能捕获变量。cangjie// Case 1

foreign func free(ptr: CPointer): Unit

// Case 2

@C

func callableInC(ptr: CPointer) {

print("This function is defined in Cangjie.")

}

// Case 3

let f1: CFunc<(CPointer) -> Unit> = { ptr =>

print("This function is defined with CFunc lambda.")

}12345678910111213以上三种形式声明/定义的函数的类型均为 CFunc<(CPointer) -> Unit>。CFunc 对应 C 语言的函数指针类型。这个类型为泛型类型,其泛型参数表示该 CFunc 入参和返回值类型,使用方式如下:

cangjieforeign func atexit(cb: CFunc<() -> Unit>): Int321与 foreign 函数一样,其他形式的 CFunc 的参数和返回类型必须满足 CType 约束,且不支持命名参数和参数默认值。

仓颉语言支持将一个 CPointer 类型的变量类型转换为一个具体的 CFunc,其中 CPointer 的泛型参数 T 可以是满足 CType 约束的任意类型,使用方式如下:

cangjiemain() {

var ptr = CPointer()

var f = CFunc<() -> Unit>(ptr)

unsafe { f() } // core dumped when running, because the pointer is nullptr.

}12345注意:

将一个指针强制类型转换为 CFunc 并进行函数调用是危险行为,需要用户保证指针指向的是一个切实可用的函数地址,否则将发生运行时错误。

unsafe 关键字 ​在引入与 C 语言的互操作过程中,同时也引入了 C 的许多不安全因素,因此在仓颉中使用 unsafe 关键字,用于对跨 C 调用的不安全行为进行标识。

关于 unsafe 关键字,有以下几点说明:

unsafe 可以修饰函数、表达式,也可以修饰一段作用域。被 @C 修饰的函数,被调用处需要在 unsafe 上下文中。在调用 CFunc 时,使用处需要在 unsafe 上下文中。foreign 函数在仓颉中进行调用,被调用处需要在 unsafe 上下文中。当被调用函数被 unsafe 修饰时,被调用处需要在 unsafe 上下文中。使用方式如下:

cangjieforeign func rand(): Int32

@C

func foo(): Unit {

println("foo")

}

var foo1: CFunc<() -> Unit> = { =>

println("foo1")

}

main(): Int64 {

unsafe {

rand() // Call foreign func.

foo() // Call @C func.

foo1() // Call CFunc var.

}

0

}12345678910111213141516171819需要注意的是,普通 lambda 无法传递 unsafe 属性,当 unsafe 的 lambda 逃逸后,可以不在 unsafe 上下文中直接调用而未产生任何编译错误。当需要在 lambda 中调用 unsafe 函数时,建议在 unsafe 块中进行调用,参考如下用例:

cangjieunsafe func A(){}

unsafe func B(){

var f = { =>

unsafe { A() } // Avoid calling A() directly without unsafe in a normal lambda.

}

return f

}

main() {

var f = unsafe{ B() }

f()

println("Hello World")

}123456789101112编译和链接 ​方法一:使用命令行参数 ​sh# 编译,在当前目录下查找 libdemo.dll

cjc -L . -l demo ./main.cj12其中:

-L . 指定库文件搜索路径为当前目录-l demo 指定要链接的库名为 demo(会自动查找 libdemo.dll)方法二:使用 CJPM 配置 link-option ​也可以用 CJPM 配置 link-option,然后就可以用 cjpm build 编译了。

toml[dependencies]

[package]

cjc-version = "0.57.3"

compile-option = ""

description = "nothing here"

link-option = "-L . -l demo"

name = "call_c"

output-type = "executable"

override-compile-option = ""

src-dir = ""

target-dir = ""

version = "1.0.0"

package-configuration = {}1234567891011121314方法三:通过配置 ffi.c 链接(推荐) ​这是最推荐的方式,通过在配置文件中指定 ffi.c 来链接库:

toml[ffi.c]

demo = { path = "libs" } # libs/libdemo.dll12这种方式更加清晰和模块化,便于管理依赖。

注意事项和最佳实践 ​内存管理:在调用 C 函数时,需要手动管理内存,使用 LibC.malloc 分配内存,使用 LibC.free 释放内存。

类型一致性:确保仓颉中的结构体定义与 C 中的结构体完全一致,包括字段顺序和类型。

@C 注解:所有与 C 交互的结构体都需要使用 @C 注解标记。

unsafe 块:涉及指针操作的部分需要放在 unsafe 块中。

错误处理:C 函数可能返回错误码,需要在仓颉代码中适当处理。

跨平台兼容性:在不同平台上,库文件的名称和格式可能不同(Windows 上是 .dll,Linux 上是 .so,macOS 上是 .dylib)。

常见问题解决 ​找不到库文件:确保库文件在正确的路径下,并且文件名正确。

符号未导出:在 Windows 上使用 extern "C" 防止 C++ 名称修饰,并确保 CMake 配置正确。

类型不匹配:仔细检查仓颉和 C 之间的类型映射关系。

内存泄漏:确保每次 malloc 都有对应的 free 调用。

🌟 相关推荐

袋装土豆(19张):种植步骤和蔬菜种植技术
365bet开户地址

袋装土豆(19张):种植步骤和蔬菜种植技术

📅 02-07 👀 8631
簡易動畫製作6步驟,用PhotoShop影格動畫做出GIF
365bet体育投注网站

簡易動畫製作6步驟,用PhotoShop影格動畫做出GIF

📅 10-14 👀 9372
温州足球小将征战“小世界杯”,展现风采收获友谊
365体育直播下载安装

温州足球小将征战“小世界杯”,展现风采收获友谊

📅 08-22 👀 7102