Sric编程语言
内存安全的系统级编程语言,设计用来取代C++。它基于所有权语义,提供运行时内存安全检查,并且生成人类可读的C++代码。
特性
- 极限性能:支持底层内存操作,没有垃圾收集器,和C++一样快。
- 可选内存安全:几乎没有内存泄漏,没有悬垂指针等C++常见内存问题。
- 容易学习:没有各种版本构造/赋值函数、模板元编程、函数重载,没有借用检查、没有生命期标注。
- 和C++交互:生成人类可读的C++代码。重用已有代码和工具。
- 现代特性:面向对象、空指针安全、动态反射、模板泛型、闭包、协程等特性。
- 工具:VSCode插件和 LSP 支持。
为什么要用Sric
Sric语言大部分灵感来自C++,在C++基础上做了两件事情:
- 减少特性、减少复杂性。
- 增加内存安全性。
设计哲学
性能
“现代计算机性能足够快”是个童年的谎言,硬件的发展远赶不上问题复杂度的增长。我遇到的每个行业都有性能问题。要解决性能问题,首先就不能有GC。有垃圾收集的语言始终存在隐形的性能天花板。需要有底层内存操作的能力,并且能灵活的在栈中分配内存,这是高性能语言需要具备的条件。Sric从设计之初就拥有和C/C++一样的性能。
内存安全
内存安全和性能是可兼得的。但Rust的机制却限制了代码的功能,强迫用户编写复杂且低效的代码,并增加了学习成本。Sric则采用了另一种方式,对开发者来说不需要做任何事情就能获得内存安全。Sric内存安全原理
抽象能力
面向对象是编程语言抽象能力的重要表现。尽管由于滥用继承导致现今人们对面向对象有些不好的印象,但我觉得面向对象在某些场合还是有用的。并且从语言上对继承做了一些限制。
简单和容易
C++和Rust都走到了这条的对立面,企图用复杂的特性覆盖所有。Sric则试图尽可能减少特性,避免复杂性。例如没有C++的各种版本构造函数、没有函数重载、没有模板元编程、没有多继承, 没有Rust的借用检查、宏、复杂的包模块机制、生命期标注等。
Sric并不致力于提供语法糖。过多的语法糖会提高学习成本,有时候反而降低可读性。语法糖不是越多越好,而是权衡学习成本和收益。
C++交互
Sric能够与C++无缝隙交互,它能生成人类可读的C++代码。C++和C历史悠久,有大量优秀的第三方库。Sric可以融入C++生态,轻松调用遗产代码,或者非常容易的调用操作系统API。
并且Sric代码也可以容易的被C++语言调用,这样能与不愿意学习Sric的开发者更好的协作。例如我们可以内部使用Sric开发,对外部提供C++接口。
数据类型
var p: Int //值类型
var p: own* Int //所有权指针
var p: * Int //非所有权指针
var p: raw* Int //裸指针
明确的拷贝或者移动
移动或者共享所有权指针
var p: own* Int = ...
var p1 = p; //compiler error
var p2 = move p
var p3 = share(p2)
移动或者拷贝有所有权指针的结构:
struct A {
var i: own* Int
fun copy(): A { ... }
}
var a: A
var x = a //compile error
var b = move a
var c = a.copy()
Unsafe
在unsafe块中解引用裸指针
var p: raw* Int
...
unsafe {
var i = *p
}
Unsafe 函数必须在unsafe块中调用
unsafe fun foo() { ... }
fun main() {
unsafe {
foo()
}
}
继承
和Java一样的单继承
trait I {
virtual fun foo()
}
virtual struct B {
var a: Int
fun bar() { ... }
}
struct A : B, I {
override fun foo(B* b) {
...
}
}
With块
with块不是C++的命名初始化, 它可以包含任何语句.
struct A {
var i: Int
fun init() { ... }
}
var a = A { .init(); .i = 0 }
var a: own* A = new A { .i = 0 }
指针用法
总是通过.来访问
var a: A
var b: own* A
a.foo()
b.foo()
类型转换和判断
var a = p as own* A
var b = p is own* A
数组
静态大小的数组
var a = []Int { 1,2,3 }
var a: [15]Int
泛型类型
泛型类型通过$<开头
struct Bar$<T> {
fun foo() {
...
}
}
T fun foo$<T>(a: T) {
return a
}
var b: Bar$<Int>
Null 安全
指针默认是不可以为null的
var a: own*? B
var b: own* B = a
可空类型转为不可空类型会在运行时进行检测,防止空值传播。
不可变性
和C++类似
var p : raw* const Int
var p : const raw* Int
var p : const raw* const Int
可见性
public
private
protected
readonly
readonly 的意思是公开读,私有写。
操作符重载
struct A {
operator fun mult(a: A): A { ... }
}
var c = a * b
可重载的操作符:
methods symbol
------ ------
plus a + b
minus a - b
mult a * b
div a / b
get a[b]
set a[b] = c
compare == != < > <= >=
add a,b,c;
模块
模块是命名空间,也是编译单元和部署单元。
一个模块包含好多源文件和文件夹.
模块通过构建脚本来定义:
name = hello
summary = hello
outType = exe
version = 1.0
depends = sric 1.0
srcDirs = ./
在代码里面导入外部模块:
import std::*
import std::Vec
闭包
fun foo(f: fun(a:Int) ) {
f(1)
}
foo(fun(a:Int){ ... })
类型别名
别名:
typealias VecInt = std::Vec$<Int>
枚举
enum Color {
red = 1, green, blue
}
var c = Color::red
默认参数和命名参数
fun foo(a: Int, b: Int = 0) {
}
fun main() {
foo(a : 10)
}
协程
async fun test2() : Int {
var i = 0
i = await testCallback()
return i + 1
}
从C++到Sric
类型比较
| C++ | Sric |
|---|---|
| int | Int |
| short | Int16 |
| int32_t | Int32 |
| unsigned int | UInt32 |
| int64_t | Int64 |
| uint64_t | UInt64 |
| float | Float/Float32 |
| double | Float64 |
| void | Void |
| char | Int8 |
定义
| C++ | Sric |
|---|---|
| const char* str | var str: raw* Int8 |
| void foo(int i) {} | fun foo(i: Int) {} |
| char a[4] | var a: [4]Int8 |
| const int& a | var a: & const Int |
类型定义
C++
#include <math.h>
class Point {
public:
int x
int y
double dis(const Point &t) const {
int dx = t.x - x;
int dy = t.y - y;
return sqrt(dx*dx + dy*dy);
}
};
Sric:
import sric::*;
struct Point {
var x: Int
var y: Int
fun dis(t: & const Point) const: Float {
var dx = t.x - x
var dy = t.y - y
return sqrt(dx*dx + dy*dy)
}
}
特性比较
从C++移除的功能
- 没有函数重载
- 没有头文件
- 没有大对象的隐式拷贝
- 不能一句定义多个变量
- 没有嵌套类、嵌套函数
- 没有class, 只有struct
- 没有命名空间
- 没有宏
- 没有向前声明
- 没有static的三重意思
- 没有友元
- 没有多继承
- 没有虚继承和私有继承
- 没有i++,只有++i
- 没有switch语句自动贯穿
- 没有模板特化
- 没有各种各样的构造函数
比C++多的
- 简单和容易
- 内存安全
- 模块化(模块化在这里有不同的定义)
- With块
- 不可空指针
- 动态反射
- 命名参数
内存安全原理
内存安全、性能、简单性构成一个不可能三角,选择其中两个必定舍弃另外一个。Sric创新性的采用运行时内存安全检查方法,在零开销的同时,减少了内存安全问题。
为什么不用Rust
Sric和Rust都是没有GC的内存安全语言,但两者有很大差别。Rust在编译时做安全检查,而Sric是在运行时检查内存安全。Rust的安全机制有很多编码限制,会强迫用户写复杂的代码。这些复杂代码不但损害可读性,而且往往不是零开销的。Sric的安全机制是无感的,不需要做什么事就能获得内存安全。
可选的内存安全
尽管Sric的内存安全检查开销非常小,但为了实现零开销,最大化性能,默认只在Debug模式下开启安全检查。所以标准的工作流程是在Debug模式下调试好代码,确保没有内存问题后,再编译成Release来部署。
也就是说Sric不是完全内存安全的,安全可以根据项目性质来选择。如果项目对安全性要求超过性能,可以通过编译宏来在Release模式中打开内存安全功能。
对象生命期检查
Sric的内存安全检查包括:数组越界检查、空指针检查、野指针指针检查、悬垂指针检查等。因为使用所有权机制,也不存在内存泄漏、双重释放等问题。其他检查项都比较简单,内存安全核心问题是怎么检测悬垂指针,也就是验证内存的生命期。
在Sric中非所有权指针是一个胖指针,它包括实际指针以及一个检测码等内容。对象内存内部也有一个相同的检测码。在创建指针的时候,指针的检测码和对象的检测码是一致的。当内存释放时,这个检测码被赋0。每次在使用指针时对比指针的检测码和对象的检测码是否相同,如果不同则说明对象内存已经释放掉了。这时候及时报错,不让错误传播到其他地方,这样很容易定位到问题。
虽然原理比较简单,但是需要考虑衍生指针怎么处理(指针指向内存中部,而不是内存头)、内存数组怎么处理、堆上分配和栈上分配怎么处理、非合作对象怎么处理(C++中定义的类没有地方存检测码)。幸运的是Sric已经把大部分问题都解决了。
与Address Sanitizer比较
Address Sanitizer也被用来做内存安全检测。但Address Sanitizer有几个缺点:
- 漏检问题。例如没法检测内存释放以后又被重新分配给其他对象的情况。
- 开销问题。Address Sanitizer的内存占用和运行开销比较大。
- 不能跨平台。对Clang、GCC支持的比较好,其他编译器没有好的支持。有些实现也依赖于ARM CPU架构。
- 依赖的第三方库需重新编译。
Sric工作在语言层面而不是系统层。Sric检查的更全面,开销更少,在任何支持C++的平台上都可以用。
安装
1.安装需要的软件
安装以上软件,并配置环境变量,确保java、jar、cmake等命令在git bash中可用。
2.构建fmake
git clone https://github.com/chunquedong/fmake.git
cd fmake
sh build.sh
并将fmake/bin目录加入到环境变量PATH中
在Windows系统上使用微软C++编译器工具集:
cd fmake
source vsvars.sh
cd -
3.构建jsonc
git clone https://github.com/chunquedong/jsonc.git
cd jsonc
sh build.sh
4.构建Sric
git clone https://github.com/sric-language/sric.git
cd sric
chmod a+x bin/sric
sh build.sh
sh build_debug.sh
添加sric的"bin"目录到你的环境变量(配置完环境变量需要重启git bash)
安装
IDE
- 在vscode插件市场中搜索'sric-language',并安装。
- 在插件的设置页配置sricHome指向sric目录(bin的上一级)。
配置好后重启vscode。如果有跳转到定义、自动完成、大纲视图等功能,说明配置成功。如果重新编译sric源码,需要先关闭vscode。
Hello World
- 创建一个空文件夹作为工作空间
- 创建文件main.sric,内容如下:
import sric::*
fun main(): Int {
printf("Hello World\n")
return 0
}
- 创建module.scm文件,内容如下:
name = hello
summary = hello
outType = exe
version = 1.0
depends = sric 1.0
srcDirs = ./
- 编译和运行
sric module.scm -fmake
构建debug版本:
sric module.scm -fmake -debug
注意: 如果在Windows系统上使用微软C++编译器工具集,每次打开命令终端都需要先运行:
cd fmake
source vsvars.sh
cd -
- 运行
编译后控制台会打印输出文件路径,加上引号来运行。例如:
'C:\Users\xxx\fmakeRepo\msvc\test-1.0-debug\bin\test'
使用fmake构建
不加-fmake构建后,只生成C++代码(位置在 "sric/output" 目录)。
sric hello.scm
然后再单独手动运行fmake编译:
fmake output/hello.fmake -debug
调试
可通过生成IDE项目来调试生成的C++代码。
fmake output/hello.fmake -debug -G
生成的项目文件在上层目录的build文件夹下。
猜数字游戏
import sric::*
fun main(): Int {
//init random seed
srand(currentTimeMillis())
//random 0..50
var expected = (rand() / (RAND_MAX as Float32) * 50) as Int
var guess = 0
printf("Please input your guess\n")
while (true) {
//get input
scanf("%d", (&guess as raw*Int))
if (guess > expected) {
printf("Too big\n")
}
else if (guess < expected) {
printf("Too small\n")
}
else {
printf("You win!\n")
break
}
}
return 0
}
内建类型
- Int, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64
- Float, Float32, Float64
- Bool
- Void
Int默认32位,Float默认32位。
字符串
字符串可以多行
var s = "ab
cd"
三引号字符串,密码的双引号不需要转义。
var s = """ab"
cd"""
字符串字面量的类型是raw*const Int8, 可以自动转为sric::String
字符
字符是指单个字母,类型是Int8
var c : Int8 = 'A'
注释
单行注释
//comment
多行注释
/*
comment
*/
文档注释
/**
introduce
*/
注解
//@method: GET
注解可以通过反射接口动态获取。
变量定义
- 使用var定义变量,不管是否可变都用var。
- 变量类型写变量后。
- 每个语句只能定义一个变量。
var i: Int = 0
只有函数内的局部变量才支持类型推断
var i = 0
变量自动初始化为默认值,如果想保持随机值,则使用uninit关键字。
var i = uninit
全局变量必须是不可变的,除非加unsafe修饰
var i: const Int = 0
函数定义
- 函数使用fun开始
- 返回值是Void时,可省略返回值
- 函数的名称必须是唯一值的,不支持通过参数重载
fun foo(a: Int): Int { return 0 }
fun foo2() {}
默认参数和命名参数
fun foo(a: Int, b: Int = 0) {}
foo(a: 1)
命名参数让你能显式写出参数名称,增加可读性。
前向声明
没有类似于C/C++的前向声明,前面的函数也能调用后面的。 因为sric采用的是多层编译的编译器架构。
可见性
变量和函数都支持可见性标记
public
private
protected
readonly
例如
private fun foo() {}
readonly var i: Int = 0
- 全局变量和函数的可见性保护区域是当前文件,如果声明为private则外部文件不可见。
- 默认的可见性都是public,所有不用写任何public。
- protected表示当前模块内可见,或者继承的子类可见。
- readonly只能用来修饰变量,不能修饰函数。表示公开读,私有写。
值类型
Sric中变量默认是值类型的,值类型在传递和赋值的时候会自动拷贝。而指针类型在传递和赋值的时候只拷贝了指针本身,指针指向的对象没有被拷贝。
指针
- 指针分为所有权指针、非所有权指针、裸指针。
- 和C/C++不同的是,指针的*号放在类型的前方。
var p: Int //值类型
var p: own* Int //所有权指针
var p: * Int //非所有权指针
var p: & Int //引用
var p: raw* Int //裸指针
var p: uniq* Int //唯一所有权指针
Sric也有和C++类似的一系列智能指针,以库的形式提供。包括SharedPtr、WeakPtr等,见标准库相关章节的描述。
内存分配
指针可以通过值类型取地址获得,但更常用的是通过new关键字在堆上分配内存。
var i: own* Int = new Int
new后面的类型没有括号(因为Sric不支持有参构造函数)。new分配的对象为所有权指针。
所有权指针
- 所有权指针表示拥有所指对象,在作用域结束时自动释放对象。
- 传递或者赋值所有权指针时需要显式移动或者拷贝。
var p1: own* Int = ...
var p2 = move p1
var p3 = share(p1)
使用share函数,从一个所有权指针分裂出新的所有权指针,多个指针指向同一个内存地址。
唯一所有权指针
uniq*是零开销的。和own*类似,但没有share()方法。
var p1: uniq* Int = makeUniq$<Int>()
var p2 = move p1
非所有权指针
- 非所有权指针使用没有限制,不会像Rust那样有借用限制。
- 程序会在运行时检查所引用的对象是否有效。
own*和uniq*可以自动转换成非所有权指针和裸指针。
var p1: own* Int = ...
var p4: * Int = p1
var p5: raw* Int = p1
裸指针
裸指针是C/C++的指针,需要在安全模式中使用。
var p: raw* Int = ...
unsafe {
var i = *p
}
其他类型的指针可以自动转为裸指针类型。
取地址
值类型取地址运算后获得其指针。本地字段取地址后为非所有权指针(数组除外)。
var i: Int = 0
var p: *Int = &i
指针运算
只有裸指针才能进行指针运算。指针运算只能在unsafe块,或者unsafe函数中进行。
var p : raw* Int = ...
unsafe {
++p
p = p + 2
}
引用
引用的概念同C++,但在sric中引用只能用在函数参数和返回值中。
fun foo(a: & const Int) {
}
引用可以理解为自动解引用的指针。给引用赋值会修改原始的内容。
数组
数组特指静态大小数组。如果需要动态数组,请参考标准库中的DArray。
数组的括号写在类型前面
var a: [5]Int
数组初始化时可以省略数组大小,由编译器自动推断
var a = []Int { 1,3,4 }
数组大小也可用constexpr变量来指定。
constexpr var size: const Int = 15
var a: [size]Int
目前constexpr变量只能通过字面量初始化。
空指针安全
指针默认是不可以为null的,除非加问号声明。
var a: own*? B = null
var a: own* B = null //compile error
在必要时编译器会自动插入空指针检查,以保证空指针安全。
只有指针可以赋值为null,值类型和引用不能为空。
不可变性
和C++类似,const可以修饰指针本身,也可以修饰指针所指对象。
var p : raw* const Int
var p : const raw* Int
var p : const raw* const Int
语句
- 语句结尾的分号是可以省略的。
- 不支持do while语句和goto语句,其他和C++一致。
- switch语句不支持自动贯穿,如果想贯穿需要用fallthrough关键字
switch (i) {
case 1:
fallthrough
case 2:
printf("%d\n", i)
}
Unsafe
在unsafe块中解引用裸指针
var p: *Int
...
unsafe {
var i = *p
}
Unsafe 函数必须在unsafe块中调用
unsafe fun foo() { ... }
fun main() {
unsafe {
foo()
}
}
表达式
- 除了位运算的优先级外其他和C/C++相同,位运算优先级高于等于运算符。
if (i & Mask != 0) {}
//same as
if ((i & Mask) != 0) {}
- 只支持
++i,不支持i++
With块
with块不是C++的命名初始化, 它可以包含任何语句.
struct A {
var i: Int
fun init() { ... }
}
var a = A { .init(); .i = 0 }
var a: own* A = new A { .i = 0 }
指针访问
指针也通过.来访问,不用->
var a: A
var b: own* A
a.foo()
b.foo()
类型转换和判断:
as表达式用来做动态类型转换和数字类型转换。
var a = p as own* A
var b = p is own* A
其他类型转换使用unsafeCast函数来完成。
结构
类没有有参构造函数,需要调用者自己初始化。
struct Point {
var x: Int = 0
var y: Int = uninit
var z: Int
}
var p = Point { .y = 1 }
初始化的语法叫做with块,不同于C的命名初始化。with块可以包含任何语句,并且可以用在非初始化场景。例如
var point = Point { .y = 1 }
point {
.x = 2; if (a) { .y = 3 }
}
方法
- 类型可以由方法,非静态成员函数,有一个隐藏的this指针。
- 方法必须出现在数据成员之后。
- this的可变性修饰在函数名称后,例如:
struct Point {
fun length() const : Float {
...
}
}
静态成员
struct可以包含静态函数和字段。静态函数没有隐式的this指针。
strcut Point {
static fun foo() {
}
}
Point::foo()
继承
- 不支持多继承,类似于Java
- 被继承的类需要标记为
virtual或者abstract - Trait相当与Java的interface,不能有数据成员和方法实现。
- 继承的分号后面必须先写类(如果有)再写Trait。
- 重写父类的virutal或者abstract方法时,需要加
override标记。 - 使用
super关键字调用父类方法。
virtual strcut B {
virtual fun foo() {}
}
trait I {
abstract fun foo2();
}
struct A : B , I {
override fun foo2() {}
}
构造函数和析构函数
Sric没有C++类似的构造函数,只有默认构造函数,不能有参数。例如
struct A {
fun new() {
}
fun delete() {
}
}
Sric绝大部分情况是不需要写析构函数的,因为所有权机制会自动清理内存。
构造函数是为了弥补原地初始化不能写复杂逻辑的问题。例如可以用三个点的语法,在构造函数中初始化。
struct A {
var p : own* Int = ...;
fun new() {
p = new Int
}
}
类型别名
类型别名相当于C的typedef
typealias size_t = Int32
枚举
枚举和C++相同,但总是占命名空间。
enum Color {
Red, Green = 2, Blue
}
fun foo(c: Color) {}
foo(Color::Red)
可以设置大小:
enum Color : UInt8 {
Red, Green = 2, Blue
}
不安全结构
unsafe结构完全和对应的C++类一致,不包含安全检查需要的标记位。extern结构默认是unsafe的。
unsafe里的this的类型是裸指针,而不是安全指针。如果对象是独立用new关键字分配的,可以通过rawToRef转为安全指针。
unsafe struct A {
fun foo() {
var self = rawToRef(this)
}
}
dconst方法
为了减少代码重复,以及函数重载这种复杂特性,Sric提供了dconst方法。
struct A {
var i: String
fun foo() dconst : * String {
...
return &i
}
}
dconst方法在编译器内部会自动生成cosnt和非const两个版本的。等价于下面的C++代码:
class A {
string i;
public:
string* foo() {
...
return &i;
}
const string* foo() const {
...
return &i;
}
};
模板
模板定义
模板和C++不同的是使用$<开头,这是为了消除泛型参数和小于运算符的歧义。
struct Tree$<T> {
}
模板参数可以有示例类型,编译时以示例类型来做类型检查。例如:
abstract struct Linkable$<T> {
var next: own*? T
var previous: *? T
}
struct LinkedList$<T: Linkable$<T>> {
}
模板实例化
泛型模板实例化时,可以传入任意满足示例类型的类型。
var tree = Tree$<Int> {}
闭包/Lambda
匿名函数使用fun关键字定义
fun foo(f: fun(a:Int):Int) {
f(1)
}
foo(fun(a:Int):Int{
printf("%d\n", a)
return a + 1
})
暂不支持闭包的返回类型推断,需要显式指定返回类型。
捕获变量
默认通过值来捕获外部变量。
var i = 0
var f = fun(a:Int):Int{
return a + i
}
可变闭包
默认捕获的变量是不可变的
var i = 0
var f = fun(a:Int) mut {
i = 1
}
静态闭包
静态闭包指无状态的闭包,用static修饰。不可捕获变量。
var f : fun(a:Int) static : Int
引用捕获
不支持C++的引用捕获。如果想引用捕获,可以自己取地址。
var i = 0
var ri = &i
var f = fun(a:Int):Int{
return a + *ri
};
移动捕获
不支持C++的移动捕获。AutoMove用来包装对象,避免显式使用move指令:
var arr: DArray;
var autoMove = AutoMove { .set(arr) }
var f = fun() {
var s = autoMove.get().size()
}
操作符重载
使用operator关键字来实现操作符重载。
struct A {
operator fun mult(a: A): A { ... }
}
var c = a * b;
可重载的操作符:
methods symbol
------ ------
plus a + b
minus a - b
mult a * b
div a / b
get a[b]
set a[b] = c
compare == != < > <= >=
add a,b,c;
逗号运算符
逗号运算符只在with块内有效.
x { a, b, c }
等价于
x { .add(a).add(b).add(c); }
模块化
sric中模块既是命名空间,也是编译单元,也是部署单元。软件由多个相互依赖的模块组成。
模块定义
模块通过构建脚本来定义,构建脚本以scm为扩展名:
name = hello
summary = hello
outType = exe
version = 1.0
depends = sric 1.0
srcDirs = ./
源码目录srcDirs需要以/结尾,编译器会自动搜索目录下的所有.sric文件。
模块导入
在代码里面导入外部模块:
import sric::*
import sric::DArray
其中*表示导入模块下的所有符号。导入的模块必须在构建脚本的depends字段声明。
标准库
概述
- sric: 自带标准库
- jsonc: JSON解析和压缩库
- serial: 序列化库,基于动态反射功能。
- sricNet: 网络库,支持native和webassembly
使用C语言库
cstd模块只导出了一部分常用的C函数,期待您的补充。
如果使用到没有导出的C语言库,可自行导出。例如:
externc fun printf(format: raw* const Int8, args: ...)
fun main() {
printf("Hello World\n")
}
使用externc声明即可。宏按照const变量来声明。 更多请参见C++交互。
String
在Sric中字符串是raw* const Int8类型的,可以自动转为String类型。
var str: String = "abc"
有些情况下可能需要手动转String:
var str = asStr("abc")
DArray
DArray类似于C++的std::vector。用于存储需要动态增长大小的数据。
var a : DArray$<Int>
a.add(1)
a.add(2)
verify(a[0] == 1)
HashMap
HashMap用来存储key-value数据。
var map = HashMap$<Int, String>{}
map.set(1, "1")
map.set(2, "2")
verify(map[2] == "2")
读写文件
可用FileStream来读写文件。
写入文件:
var stream = FileStream::open("tmp.txt", "wb")
stream.writeStr("Hello\nWorld")
读取文件:
var stream = FileStream::open("tmp.txt", "rb")
var line = stream.readAllStr()
第二个参数的含义,参见C语言的fopen函数。
智能指针
Sric提高了类似与C++的一组智能指针。包括SharedPtr、WeakPtr等。
SharedPtr
SharedPtr是自动引用计数的,有一定的开销。可从现有的own*获取。例如:
var p = new Int
var sp: SharedPtr$<Int> = toShared(p)
SharedPtr可通own*相互转换:
var p = sharedPtr.getOwn()
sharedPtr.set(p)
WeakPtr
使用own*和SharedPtr都可能产生循环引用,导致内存泄漏。可使用WeakPtr来打破循环引用。
var p = new Int
var wp: WeakPtr$<Int> = toWeak(p)
使用时通过lock方法转化为own*,然后进行使用。
var sp : own*? Int = wp.lock()
如果WeakPtr引用的对象已经被释放,则lock方法返回null。
Wase
Wase是Sric开发的跨平台UI库,一套代码可同时编译运行在桌面端和Web浏览器中,未来将支持Android和iOS开发。Wase有着丰富的控件和优美的界面。支持配置文件创建UI,支持自定义样式。Wase使用自绘的方式现实,类似于Qt和Flutter,但是更加轻量级。Sric语言专门为其设计了逗号表达式语法,使它的声明式API类似于SwiftUI和React Native。
learn more: https://github.com/sric-language/wase
反射
默认不启用反射,需要手动添加reflect标记。
reflect struct Point {
var x: Int
var y: Int
}
注解可以在反射API中获取
//@SimpleSerial
reflect struct Point {
var x: Int
var y: Int
}
用法
import sric::*
import serial::*
import waseGraphics
unsafe fun main(): Int {
//force load reflect metadata
var c : waseGraphics::Color
c.init(1, 1, 1, 1);
var rmodule = findModule("waseGraphics")
printf("rmodule %p\n", rmodule as raw*? Void)
//make instance
var rtype = findRType("waseGraphics::Color")
var obj = newInstance(*rtype)
//access field
var rgba = (unsafeCast$<raw*Int8>(obj) + rtype.fields[0].offset) as raw*UInt32
*rgba = 0xff8845ff
//call method
var rmethod = findInstanceMethod(rtype, "toString")
var str = callInstanceToString(rmethod.pointer, obj)
printf("%s\n", str.c_str())
return 0
}
协程
Sric的协程和Javascript的几乎完全一致,下面是协程的例子:
async fun test2() : Int {
var i = 0
i = await testCallback()
printf("await result:%d\n", i)
return i + 1
}
- 协程函数使用async标记
- await的目标必须是async函数,或者返回值为
Promise$<T>的函数。 - async函数的返回值会被编译器自动包装成
Promise$<T>
协程C++适配
接入主循环
Sric协程通过主循环来调度。UI框架的主线程有主循环,服务端库libevent、libev、libuv、libhv都有主循环。他们的主循环写法都不太一样,伪代码如下:
sric::call_later = [](std::function<void()> h){
call_in_loop([]{
h();
});
};
适配异步回调接口
堆上分配sric::Promise
sric::Promise<int> testCallback() {
auto resultData = std::make_shared<sric::Promise<int>::ResultData >();
std::thread([=]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
resultData->on_done(1);
}).detach();
return sric::Promise<int>(resultData);
}
和C++交互
sric可以很容易的和C++交互。sric编译为人类可读的C++代码,可以像C++代码一样被C++直接调用。
调用C++代码,只需要将C++代码的原型声明一下,即可调用。C语言/无命名空间代码使用externc来修饰;同名命名空间使用extern来修饰;其他情况使用符号映射。
C语言/无命名空间
externc fun printf(format: raw* const Int8, args: ...)
fun main() {
printf("Hello World\n")
}
同名命名空间
当C++命名空间和模块名称相同时。 C++:
namespace xx {
class P {
void foo();
};
}
Sric:
//xx module
extern struct P {
fun foo()
}
这种情况下Sric代码的模块名称必须和C++的命名空间一致。
符号映射
也可以用symbol注解来映射符号名称,例如。 C++:
namespace test {
void hi() {
}
}
Sric:
//@extern symbol: test::hi
extern fun hello()
此时在Sric中调用hello将调用C++的hi方法。
包含头文件
在顶级声明前面用@#include注解来包含特殊的C++头文件
//@#include "test.h"
有参构造函数
由于Sric不支持有参数的构造函数,所以使用makePtr,makeValue来调用有参数的构造函数。
完整示例
import sric::*;
//@#include <vector>
//@extern symbol: std::vector
extern struct vector$<T> {
fun size(): Int
}
fun testExtern() {
var v = makePtr$<vector$<Int>>(3)
verify(v.size() == 3)
}
fun testExtern2() {
var v2 = makeValue$<vector$<Int>>(3)
verify(v2.size() == 3)
}
从C++头文件生成Sric接口
使用tool目录的python脚本,可以由C++头文件生成的sric原型。
不使用fmake进行编译
可以自己编译生成的C++代码,位于sric/output目录下。
可以定义SC_NO_CHECK和SC_CHECK宏。
- SC_CHECK表示进行安全检查。
- SC_NO_CHECK表示不进行安全检查。
当没有这两个宏时,按照_DEBUG和NDEBUG宏来自动定义。
Sric代码和C++代码混合编译
在module.scm中增加fmake的配置项,以fmake.前缀开头,例如:
fmake.srcDirs = ./
fmake.incDirs = ./
序列化
sric通过内建的动态反射来支持序列化,序列化格式为文本格式HiML。
序列化例子
要序列化的类需要reflect标记。例如:
reflect struct Point {
var x: Int
var y: Float
}
unsafe fun testSimple() {
var encoder: Encoder;
var obj = new Point { .x = 1; .y = 2 }
var t = obj as *Void;
var res = encoder.encode(t, "testSerial::Point")
printf("%s\n", res.c_str())
var decoder: Decoder
var p = decoder.decode(res)
var obj2: raw* Point = unsafeCast$<raw*Point>(p)
verify(obj2.x == obj.x)
verify(obj2.y == obj.y)
}
上面的例子中由于Point是非多态对象,所以要显式传入名称"testSerial::Point"。
避免序列化
有时候有些字段不想序列化,可以用Transient注解来标记
reflect struct Point {
var x: Int
//@Transient
var y: Float
}
如果本身不想序列化,可以通过_isTransient方法来动态决定
reflect struct Point {
var x: Int
fun _isTransient(): Bool {
return false
}
}
反序列化后处理
有时候希望反序列化后,调用指定函数来恢复状态。名称为_onDeserialize的函数将被自动调用。
reflect struct Point {
var x: Int
fun _onDeserialize() {
}
}
简单模式序列化
正常情况下自定义类被序列化为HiML对象,可通过SimpleSerial注解将其序列化为字符串。例如我们想要把Insets序列化为"1 2 3 4"而不是"Insets{top=1,right=2,bottom=3,left=4}"
//@SimpleSerial
reflect struct Insets {
var top: Int = 0
var right: Int = 0
var bottom: Int = 0
var left: Int = 0
fun toString() : String {
return String::format("%d %d %d %d", top, right, bottom, left)
}
fun fromString(str: String): Bool {
var fs = str.split(" ")
if (fs.size() == 4) {
top = fs[0].toInt32()
right = fs[1].toInt32()
bottom = fs[2].toInt32()
left = fs[3].toInt32()
return true
}
return false
}
}
序列化和反序列化过程中会自动调用toString和fromString,两个函数的签名必须和上面完全一致。
代码约定
源文件
源文件使用UTF-8编码。
缩进
使用4空格缩进,例如:
if (cond) {
doTrue
}
else {
doFalse
}
命名
- 类型名使用Pascal命名法
- 模块名、函数名、变量名使用小写驼峰式命名。
- 从来不要用全大写:"FOO_BAR"
常见问题
中文乱码
git bash中,运行下面命令:
cmd "/c chcp 65001>nul"
cmd中运行下面命令;
chcp 65001>nul
编译器不支持C++20
由于协程需要C++20版本,可以通过参数来指定C++版本(此时协程相关代码会编译报错)。
sric module.scm -fmake -c++17
命令行工具
用法
sric命令有以下参数:
- -debug 在Debug模式编译(默认release模式)
- -fmake 使用fmake编译生成的C++代码。
- -r 递归构建所有依赖。
- -c++17 使用C++17标准。