Sric编程语言

内存安全的系统级编程语言,设计用来取代C++。它基于所有权语义,提供运行时内存安全检查,并且生成人类可读的C++代码。

特性

  • 极限性能:支持底层内存操作,没有垃圾收集器,和C++一样快。
  • 可选内存安全:几乎没有内存泄漏,没有悬垂指针等C++常见内存问题。
  • 容易学习:没有各种版本构造/赋值函数、模板元编程、函数重载,没有借用检查、没有生命期标注。
  • 和C++交互:生成人类可读的C++代码。重用已有代码和工具。
  • 现代特性:面向对象、空指针安全、动态反射、模板泛型、闭包、协程等特性。
  • 工具:VSCode插件和 LSP 支持。

为什么要用Sric

Sric语言大部分灵感来自C++,在C++基础上做了两件事情:

  1. 减少特性、减少复杂性。
  2. 增加内存安全性。

设计哲学

性能

“现代计算机性能足够快”是个童年的谎言,硬件的发展远赶不上问题复杂度的增长。我遇到的每个行业都有性能问题。要解决性能问题,首先就不能有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
intInt
shortInt16
int32_tInt32
unsigned intUInt32
int64_tInt64
uint64_tUInt64
floatFloat/Float32
doubleFloat64
voidVoid
charInt8

定义

C++Sric
const char* strvar str: raw* Int8
void foo(int i) {}fun foo(i: Int) {}
char a[4]var a: [4]Int8
const int& avar 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.安装需要的软件

  • JDK 17+
  • 支持C++20的C++ 编译器: gcc 11+、 clang 17+、 Xcode 16+、 Visual Studio 2022+
  • CMake
  • git
  • VSCode

安装以上软件,并配置环境变量,确保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 -

更多关于fmake的信息

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

  1. 在vscode插件市场中搜索'sric-language',并安装。
  2. 在插件的设置页配置sricHome指向sric目录(bin的上一级)。

配置好后重启vscode。如果有跳转到定义、自动完成、大纲视图等功能,说明配置成功。如果重新编译sric源码,需要先关闭vscode。

Hello World

  1. 创建一个空文件夹作为工作空间
  2. 创建文件main.sric,内容如下:
import sric::*

fun main(): Int {
    printf("Hello World\n")
    return 0
}

  1. 创建module.scm文件,内容如下:
name = hello
summary = hello
outType = exe
version = 1.0
depends = sric 1.0
srcDirs = ./
  1. 编译和运行
sric module.scm -fmake

构建debug版本:

sric module.scm -fmake -debug

注意: 如果在Windows系统上使用微软C++编译器工具集,每次打开命令终端都需要先运行:

cd fmake
source vsvars.sh
cd -
  1. 运行

编译后控制台会打印输出文件路径,加上引号来运行。例如:

'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::ResultData对象,在回调中调用其on_done方法。

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标准。