基于开发者空间,实现仓颉 – C跨语言编程控制台小游戏
通过实际操作,让大家了解仓颉-C跨语言操作,使用一个简单的小游戏案例,加深大家对仓颉-C语言编程的理解。
1 概述
1.1 背景介绍
仓颉编程语言作为一款面向全场景应用开发的现代编程语言,通过现代语言特性的集成、全方位的编译优化和运行时实现、以及开箱即用的 IDE 工具链支持,为开发者打造友好开发体验和卓越程序性能。C语言是一种较早的程序设计语言,广泛应用于底层开发。C语言能以简易的方式编译、处理低级存储器。
通过实际操作,让大家了解仓颉-C跨语言操作,使用一个简单的小游戏案例,加深大家对仓颉-C语言编程的理解。
1.2 适用对象
- 个人开发者
- 高校学生
1.3 案例时间
本案例总时长预计20分钟。
1.4 案例流程
说明:
① 编译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以及仓颉工具链,开箱即用。我们可以在开发者空间直接进行仓颉开发体验。
首先,登录开发者空间,点击进入桌面进入到云主机。
在云主机桌面打开CodeArts IDE for Cangjie。
点击新建工程创建仓颉工程,名称定义为demo,产物类型选择executable。
产物类型说明:
- executable,可执行文件;
- static,静态库,是一组预先编译好的目标文件的集合;
- dynamic,动态库,是一种在程序运行时才被加载到内存中的库文件,多个程序共享一个动态库副本,而不是像静态库那样每个程序都包含一份完整的副本。
创建项目后,打开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函数生成的随机数。
2.2.2 仓颉调C语言自定义库函数
C语言自定义库函数需显式链接(如编译时加-L./ -lxxx)或运行时动态加载(dlopen/dlsym)。下面我们结合示例进行体验。
打开华为开发者空间云主机中的CodeArts IDE for Cangjie编译器,任意窗口下使用快捷键ALT+P新建仓颉工程,名称定义为demo1,产物类型选择executable。
在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目录,选择在集成终端中打开进入终端窗口。
然后执行以下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
可以看到已成功调用了C语言中的getString和printCString函数。
2.3 实现一个猜拳小游戏
接下来我们通过一个猜拳小游戏开发来练习仓颉-C跨语言编程。通过C语言生成随机选择,仓颉语言处理用户输入和胜负判定。
打开华为开发者空间云主机中的CodeArts IDE for Cangjie编译器,任意窗口下使用快捷键ALT+P新建仓颉工程,名称定义为demo3,产物类型选择executable。
在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
至此,仓颉 - C跨语言编程实现控制台小游戏案例内容全部完成。
如果想了解更多仓颉编程语言知识可以访问: https://cangjie-lang.cn/
更多推荐
所有评论(0)