1 概述

1.1 背景介绍

仓颉编程语言作为一款面向全场景应用开发的现代编程语言,通过现代语言特性的集成、全方位的编译优化和运行时实现、以及开箱即用的 IDE 工具链支持,为开发者打造友好开发体验和卓越程序性能。C语言是一种较早的程序设计语言,广泛应用于底层开发。C语言能以简易的方式编译、处理低级存储器。

通过实际操作,让大家了解仓颉-C跨语言操作,使用一个简单的小游戏案例,加深大家对仓颉-C语言编程的理解。

1.2 适用对象

  • 个人开发者
  • 高校学生

1.3 案例时间

本案例总时长预计20分钟。

1.4 案例流程

5e0307fb4be9e6d2b4aba5b1649fbab9.png

说明:

① 编译C代码为共享库;
② 编译仓颉代码并指定动态库路径;
③ 运行仓颉程序。

1.5 资源总览

资源名称 规格 单价(元) 时长(分钟)
开发者空间 - 云主机 鲲鹏通用计算增强型 kc2 | 4vCPUs | 8G | Ubuntu 免费 20

最新案例动态,请查阅 《仓颉 – C跨语言编程实现控制台小游戏》。小伙伴快来领取华为开发者空间,进入云主机桌面版实操吧!

2 仓颉-C跨语言编程

2.1 仓颉-C跨语言互操作介绍

仓颉为了兼容已有的生态,支持调用 C 语言的函数,也支持 C 语言调用仓颉的函数。

仓颉支持通过FFI(Foreign Function Interface)直接调用C语言函数。其核心机制包括:

  • 类型兼容性:仓颉的Int32、Float64等基础类型与C语言的int、double等一一对应;
  • 动态库绑定:将C代码编译为共享库(如.so或.dll),并通过@C和foreign声明函数;
  • 安全性:仓颉通过类型检查确保调用参数与返回值的正确性,避免内存错误。
    举个例子,假设要调用 C 的 rand 和 printf 函数,它的函数签名如下:
// stdlib.h
int rand();
// stdio.h
int printf (const char *fmt, ...);

那么在仓颉中调用这两个函数的方式如下:

// 通过`foreign`关键字声明函数,省略`@C`
foreign func rand(): Int32
foreign func printf(fmt: CString, ...): Int32
main() {
    // 通过`unsafe`块调用C函数
    let r = unsafe { rand() }
    println("random number ${r}")
    unsafe {
        var fmt = LibC.mallocCString("Hello, No.%d\n")
        printf(fmt, 1)
        LibC.free(fmt)
    }
}

注意:

  • foreign 修饰函数声明,代表该函数为外部函数。被 foreign 修饰的函数只能有函数声明,不能有函数实现。
  • foreign 声明的函数,参数和返回类型必须符合 C 和仓颉数据类型之间的映射关系。
  • 由于 C 侧函数很可能产生不安全操作,所以调用 foreign 修饰的函数需要被 unsafe 块包裹,否则会发生编译错误。
  • @C 修饰的 foreign 关键字只能用来修饰函数声明,不可用来修饰其他声明,否则会发生编译错误。
  • @C 只支持修饰 foreign 函数、top-level 作用域中的非泛型函数和 struct 类型。
  • foreign 函数不支持命名参数和参数默认值。foreign 函数允许变长参数,使用 … 表达,只能用于参数列表的最后。变长参数均需要满足 CType 约束,但不必是同一类型。
  • 仓颉(CJNative 后端)虽然提供了栈扩容能力,但是由于 C 侧函数实际使用栈大小仓颉无法感知,所以 ffi 调用进入 C 函数后,仍然存在栈溢出的风险(可能导致程序运行时崩溃或者产生不可预期的行为),需要开发者根据实际情况,修改 cjStackSize 的配置。
    仓颉与C语言基本数据类型映射关系:
Cangjie Type C Type Size(byte)
Unit void 0
Bool bool 1
UInt8 char 1
Int8 int8_t 1
UInt8 uint8_t 1
Int16 int16_t 2
UInt16 uint16_t 2
Int32 int32_t 4
UInt32 uint32_t 4
Int64 int64_t 8
UInt64 uint64_t 8
IntNative ssize_t platform dependent
UIntNative size_t platform dependent
Float32 float 4
Float64 double 8

* 表格展示了仓颉常用基础数据类型对应的C语言中的数据类型和字节数。int 类型、long类型等由于其在不同平台上的不确定性,需要程序员自行指定对应仓颉编程语言类型。

2.2 仓颉-C跨语言编程体验

可以简单将仓颉调用C函数分为两类,一类是调用C语言标准库。另一类是调用C语言自定义库函数。

2.2.1 仓颉调C语言标准库函数

C语言标准库程序运行时由动态链接器自动加载,可以在仓颉中直接使用foreign声明标准库函数,然后直接调用。下面我们结合示例进行体验。

华为在开发者空间云主机中预置了CodeArts IDE for Cangjie以及仓颉工具链,开箱即用。我们可以在开发者空间直接进行仓颉开发体验。

首先,登录开发者空间,点击进入桌面进入到云主机。

43f8e6fc700364501d7dd04d9c743635.png

在云主机桌面打开CodeArts IDE for Cangjie。

8bac83ade9df5e698dfe95d8366db424.png

点击新建工程创建仓颉工程,名称定义为demo,产物类型选择executable

产物类型说明

  • executable,可执行文件;
  • static,静态库,是一组预先编译好的目标文件的集合;
  • dynamic,动态库,是一种在程序运行时才被加载到内存中的库文件,多个程序共享一个动态库副本,而不是像静态库那样每个程序都包含一份完整的副本。

fa09d5ce3e9ece182b9c6218a30a7e3d.png

创建项目后,打开src目录下main.cj文件,替换成以下测试代码(仓颉调用C标准库的rand()函数生成一个随机数,并通过printf()函数输出格式化的字符串)。

package demo
import std.io.*
import std.time.*
// 声明 C 的 rand() 和 srand() 函数
foreign func rand(): Int32
foreign func srand(seed: Int64): Unit
// 声明C的printf()函数,接受CString作为第一个参数,后续参数为可变参数列表
foreign func printf(fmt: CString, ...): Int32

main() {
    // 使用当前时间纳秒作为随机数种子
    unsafe {
        srand(DateTime.nowUTC().nanosecond)
    }
    // 调用C的rand()函数生成随机数,并将其转换为0到99之间的值
    let randomNumber = unsafe { rand() } % 100
    // 使用C的printf()函数打印随机数
unsafe {
	 // 准备要打印的字符串,使用LibC.mallocCString分配C字符串
    var fmt = LibC.mallocCString("生成的随机数是:%d\n")
    printf(fmt, randomNumber)
    LibC.free(fmt)  // 释放分配的C字符串
    }
}

点击右上角运行按钮直接运行main.cj,在控制台打印仓颉调用C函数生成的随机数。
image.png

2.2.2 仓颉调C语言自定义库函数

C语言自定义库函数需显式链接(如编译时加-L./ -lxxx)或运行时动态加载(dlopen/dlsym)。下面我们结合示例进行体验。

打开华为开发者空间云主机中的CodeArts IDE for Cangjie编译器,任意窗口下使用快捷键ALT+P新建仓颉工程,名称定义为demo1,产物类型选择executable

ce78806f3f25c2aa43e8d062b5e646b2.png

在src目录下创建一个test.c文件,将下面示例代码内容复制到 test.c。

C 代码中分别提供两个函数:

  • getCString 函数,用于返回一个 C 侧的字符串指针;
  • printCString 函数,用于打印来自仓颉侧 CString 。
#include <stdio.h>

char *str = "CString in C code.";
char *getCString() { return str; }
void printCString(char *s) { printf("%s\n", s); }

将以下仓颉示例代码复制到src目录下的main.cj文件中,在此仓颉代码中,创建一个 CString 对象,传递给 C 侧打印。并且获取 C 侧字符串,在仓颉侧打印。

package demo1
foreign func getCString(): CString
foreign func printCString(s: CString): Unit

main() {
    // 仓颉侧构造 CString 实例,传递到 C 侧
    unsafe {
        let s: CString = LibC.mallocCString("CString in Cangjie code.")
        printCString(s)
        LibC.free(s)
    }
    unsafe {
        // C 侧申请字符串指针,传递到仓颉侧成为 CString 实例,再转换为仓颉字符串 String 类型
        let cs = getCString()
        println(cs.toString())
    }
    // 在 try-with-resource 语法上下文中使用 CStringResource 自动管理 CString 内存
    let cs = unsafe { LibC.mallocCString("CString in Cangjie code.") }
    try (csr = cs.asResource()) {
        unsafe { printCString(csr.value) }
    }
    
}

右键src目录,选择在集成终端中打开进入终端窗口。

52a6d302f1750e4a7ba1a241b2541391.png

然后执行以下gcc命令将C 代码编译成动态库,得到C 库 libtest.so。

gcc -fPIC -shared test.c -o libtest.so

参数说明:

  • -fPIC:生成位置无关代码;
  • -shared:生成动态链接库;
  • test.c:输入源文件;
  • -o libtest.so:指定输出文件名为动态库libtest.so。

执行cjc命令,编译出可执行文件main。

cjc -L . -l test main.cj

参数说明:

  • -L .:指定动态库搜索路径为当前目录src;
  • -l test:指定链接的动态库libtest.so;
  • main.cj:需编译的仓颉源代码文件。

执行main文件。

./main

772f1375ca9e059c40dddcdafeb5264a.png

可以看到已成功调用了C语言中的getString和printCString函数。

2.3 实现一个猜拳小游戏

接下来我们通过一个猜拳小游戏开发来练习仓颉-C跨语言编程。通过C语言生成随机选择,仓颉语言处理用户输入和胜负判定。

打开华为开发者空间云主机中的CodeArts IDE for Cangjie编译器,任意窗口下使用快捷键ALT+P新建仓颉工程,名称定义为demo3,产物类型选择executable

31ddc6764b687c9439f9711b3386d9ea.png

在src目录下创建一个game.c文件,将下面示例代码内容复制到 game.c。

 #include <stdlib.h>
#include <time.h>

// 生成电脑的随机选择(0=石头,1=剪刀,2=布)
int get_computer_choice() {
    static int initialized = 0;
    if (!initialized) {
        srand(time(NULL)); // 初始化随机种子
        initialized = 1;
    }
    return rand() % 3; // 返回0-2之间的随机整数
}

用以下代码替换main.cj文件原有代码。

package demo3
import std.io.*
import std.console.*
import std.convert.*
// 声明C函数
foreign func get_computer_choice() : Int32

main(): Unit {
    print("石头剪刀布游戏")
    print("输入数字选择: 0=石头, 1=剪刀, 2=布")
    
    while (true) {
        print("\n你的选择(0/1/2, 输入q退出): ")
        let input = Console.stdIn.readln()
        if (input == "q") {
            print("游戏结束!\n")
            break
        }
        // 获取玩家输入并转为Int32类型
        let player_choice = Int32.parse(input.getOrThrow())
        if (player_choice < 0 || player_choice > 2) {
            print("输入无效! 请输入0-2或q退出")
            continue
        }
        // 调用C函数获取电脑选择
        let computer_choice = unsafe {get_computer_choice()}
        // 判定胜负
        let result = (player_choice - computer_choice + 3) % 3
        var outcome = ""
        if (result == 0) {
            outcome = "平局!"
        } else if (result == 1) {
            outcome = "你输了!"
        } else {
            outcome = "你赢了!"
        }
        // 显示结果
        print("你:${get_name(player_choice)} | 电脑:${get_name(computer_choice)}")
        print(outcome)
        print("\n-----------------------")
    }
}

// 辅助函数:将数字转换为名称
func get_name(choice: Int32) : String {
    if (choice == 0) {
        return "石头"
    }else if (choice == 1) {
        return "剪刀"
    }else if (choice == 2) {
        return "布"
    }else {
        return "未知"
    }
}

右键src目录,选择在集成终端中打开进入终端窗口,执行以下命令运行猜拳游戏。

gcc -fPIC -shared game.c -o libgame.so
cjc -L . -l game main.cj -o game_app
./game_app

38e70a91e61c7fbde4bd5af4ebdae257.png

至此,仓颉 - C跨语言编程实现控制台小游戏案例内容全部完成。

如果想了解更多仓颉编程语言知识可以访问: https://cangjie-lang.cn/

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐