Sric Language

A memory-safe low-level systems programming language.

Features

  • Blazing fast: low-level memeory access without GC.
  • Memory safe: no memory leak, no dangling pointer.
  • Easy to learn: No borrow checking, lifetime annotations. No various constructors/assignment, template metaprogramming, function overloading.
  • Interoperate with C++: compile to human readable C++ code.
  • Modern features: object-oriented, null safe, dynamic reflection,template, closure, coroutine.
  • Tools: VSCode plugin and LSP support.

Why Sric

Sric is a high-performance programming language with memory safety. The design of Sric draws heavily from C++, but it makes two key improvements:

  1. Reducing features and complexity.
  2. Adding memory safety.

Design Philosophy

Performance

The notion that "modern computers are fast enough" is a childhood myth. Hardware advancements can’t keep up with the growing complexity of problems. Every industry I’ve encountered faces performance issues. To solve performance problems, the first step is to eliminate garbage collection (GC). Languages with GC inherently have an invisible performance ceiling. A high-performance language must provide low-level memory manipulation and flexible stack allocation. From the outset, Sric has been designed to match the performance of C/C++.

Memory Safety

Memory safety and performance can coexist. However, Rust's mechanisms impose restrictions on code functionality, forcing developers to write complex and inefficient code while increasing the learning curve.
In contrast, Sric takes a different approach—developers don’t need to do anything extra to achieve memory safety. How Memory Safety Works

Abstraction Capabilities

Object-oriented programming (OOP) is a key measure of a language’s abstraction power. Although misuse of inheritance has given OOP a bad reputation, I believe it remains useful in certain scenarios. Sric supports OOP but imposes language-level restrictions on inheritance.

Simplicity and Ease

Both C++ and Rust have gone to the opposite extreme, attempting to cover every use case with complex features. Sric, on the other hand, strives to minimize features and avoid complexity. For example: No various versions of constructors/assignment functions, function overloading, or template metaprogramming (unlike C++). No borrow checking, macros, intricate module systems, or lifetime annotations (unlike Rust).

Sric avoids excessive syntactic sugar. While syntactic sugar can reduce verbosity, too much of it increases learning costs and may harm readability. The goal is to strike a balance between ease of use and cognitive overhead.

Interoperability with C++

Sric seamlessly interoperates with C++ and can generate human-readable C++ code. C++ and C have long histories and vast ecosystems of high-quality libraries. Sric integrates smoothly into the C++ ecosystem, making it easy to leverage legacy code or call operating system APIs.

Additionally, Sric code can be easily invoked from C++, facilitating collaboration with developers who haven’t adopted Sric. For example, a team could use Sric internally while exposing C++ interfaces externally.

Data Type

var p: Int             //value type
var p: own* Int;       //ownership pointer
var p: * Int;          //non-owning pointer
var p: & Int;          //reference
var p: raw* Int;       //unsafe raw pointer
var p: uniq* Int;      //unique pointer

Explicit Copy or Move

Move or share ownership pointer

var p: own* Int = ...;
var p1 = p; //compiler error;
var p2 = move p;
var p3 = share(p2);

Move or copy a struct with ownership pointer:

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

Dereference a raw pointer in an unsafe block/function:

var p: raw* Int;
...
unsafe {
    var i = *p;
}

Unsafe functions must be called in an unsafe block/function:

unsafe fun foo() { ... }

fun main() {
    unsafe {
        foo();
    }
}

Inheritance

Single inheritance, similar to Java:

trait I {
    virtual fun foo();
}

virtual struct B {
    var a: Int;
    fun bar() { ... }
}

struct A : B, I {
    override fun foo(B* b) {
        ...
    }
}

With-Block

A with-block is not like C++'s named initialization; it can contain any statements:

struct A {
    var i: Int;
    fun init() { ... }
}

var a  = A { .init(); .i = 0; };
var a: own* A = new A { .i = 0; };

Pointer Usage

Always access by .

var a: A;
var b: own* A;
a.foo();
b.foo();

Type Cast and Check

var a = p as own* A;
var b = p is own* A;

Array

Statically sized arrays:

var a  = []Int { 1,2,3 };
var a: [15]Int;

Generic Type

Generic params start with $<

struct Bar$<T> {
    fun foo() {
        ...
    }
}

T fun foo$<T>(a: T) {
    return a;
}

var b: Bar$<Int>;

Null safe

Pointer is non-nullable by default.

var a: own*? B;
var b: own* B = a;

Immutable

Similar to C++:

var p : raw* const Int;
var p : const raw* Int;
var p : const raw* const Int;

Protection

public
private
protected
readonly

readonly means publicly readable but privately writable.

Operator Overloading

struct A {
    operator fun mult(a: A): A { ... }
}

var c = a * b;

Overloadable operators:

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;

Module

Module is namespace as well as the unit of compilation and deployment.

A module contains several source files and folders.

The module is defined in build scripts:

name = hello
summary = hello
outType = exe
version = 1.0
depends = sric 1.0, cstd 1.0
srcDirs = ./

import external module in code:

import std::*;
import std::Vec;

Closure

fun foo(f: fun(a:Int) ) {
    f(1);
}

foo(fun(a:Int){ ... });

Type Alias

typealias:

typealias VecInt = std::Vec$<Int>;

Enum

enum Color {
    red = 1, green, blue
}

var c = Color::red;

Default Params and Named Args

fun foo(a: Int, b: Int = 0) {
}

fun main() {
    foo(a : 10);
}

Coroutine

async fun test2() : Int {
    var i = 0;
    i = await testCallback();
    return i + 1;
}

From C++ to Sric

Types

C++Sric
intInt
shortInt16
int32_tInt32
unsigned intUInt32
int64_tInt64
uint64_tUInt64
floatFloat32
doubleFloat/Float64
voidVoid
charInt8

Defines

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

Class

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 cstd::*;

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);
    }
};

Features Compare

Removed features from C++

  • No function overload by params
  • No header file
  • No implicit copying of large objects
  • No define multi var per statement
  • No nested class, nested function
  • No class, only struct
  • No namespace
  • No macro
  • No forward declarations
  • No three static
  • No friend class
  • No multiple inheritance
  • No virtual,private inheritance
  • No i++ only ++i
  • No switch auto fallthrough
  • No template specialization
  • No various constructors

More than C++

  • Simple and easy
  • Memory safe
  • Modularization
  • With block
  • Non-nullable pointer
  • Dynamic reflection
  • Named args

How Memory Safety Works

Sric and Rust are both memory-safe languages without GC, but they differ significantly. Rust performs safety checks at compile time, while Sric checks memory safety at runtime. Rust's safety mechanisms impose many coding restrictions, forcing users to write complex and inefficient code. In contrast, Sric's safety mechanisms are transparent, requiring no extra effort to achieve memory safety. Sric's memory safety checks have minimal overhead, and by default, safety checks are disabled in Release mode, where performance matches hand-written C++ code.

Memory safety encompasses many aspects. Issues like array out-of-bounds, null pointers, and uninitialized pointers can be addressed with simple checks. Thanks to the ownership mechanism, problems like memory leaks and double-free are also eliminated. Thus, the core challenge of memory safety is detecting dangling pointers. In Sric, non-ownership pointers handle the primary task of memory detection.

In Sric, a non-ownership pointer is a "fat pointer," which includes the actual pointer and a verification code, among other details. The object's memory also contains an identical verification code. When a pointer is created, its verification code matches the object's. When memory is released, this code is set to 0. Each time the pointer is used, the verification codes of the pointer and the object are compared. If they differ, it indicates the object's memory has been freed. An error is reported immediately, preventing the issue from propagating, making it easy to locate the problem.

Although the principle is straightforward, several challenges must be addressed: handling derived pointers (pointers to the middle of memory rather than the start), managing memory arrays, distinguishing between heap and stack allocations, and dealing with non-cooperative objects (e.g., C++ classes with no space for verification codes). Fortunately, Sric has resolved most of these issues.

Address Sanitizer is also used for memory safety detection. However, it cannot detect cases where freed memory is reallocated to another object. Additionally, Address Sanitizer incurs significant memory and runtime overhead, lacks cross-platform compatibility, and requires recompilation of third-party libraries. In comparison, Sric's checks are more comprehensive, with negligible runtime overhead and no platform limitations.

Install

1.Required

Install the above software and configure the environment variables to ensure that commands such as java, jar, fan, and cmake are available in git bash.

2.Build fmake

git clone https://github.com/chunquedong/fmake.git
cd fmake
fanb pod.props

Use the Microsoft C++ compiler toolchain on Windows:

cd fmake
source vsvars.sh
cd -

About fmake

3.Build jsonc

git clone https://github.com/chunquedong/jsonc.git
cd jsonc
sh build.sh

6.Build Sric

git clone https://github.com/sric-language/sric.git
cd sric
chmod a+x bin/sric
sh build.sh
sh build_debug.sh

Add sric/bin to your PATH (restart git bash afterward).

Install

build from source

IDE

  1. Search 'sric-language' in vscode marketplace, install it.
  2. Configure sricHome to point to the sric directory (the parent directory of bin).

After configuring, restart VSCode. If features such as Go to Definition, Auto Completion, and Outline View are available, it means the configuration was successful. If you recompile the Sric source code, you need to close VSCode first.

Hello World

  1. Create an empty folder as the workspace

  2. Create a file named main.sric with the following content:

import cstd::*;

fun main(): Int {
    printf("Hello World\n");
    return 0;
}
  1. Create a module.scm file with the following content:
name = hello  
summary = hello  
outType = exe  
version = 1.0  
depends = sric 1.0, cstd 1.0  
srcDirs = ./
  1. Build
sric module.scm -fmake

build debug mode:

sric module.scm -fmake -debug
  1. Run

After compilation, the console will print the output file path. Run it with quotes. For example:

'C:\Users\xxx\fmakeRepo\msvc\test-1.0-debug\bin\test'

Build by fmake

The build process without -fmake solely outputs C++ code (under "sric/output").

sric hello.scm

Then compile it separately by manually running fmake:

fan fmake output/hello.fmake -debug

Debug

Debugging the generated C++ code is supported via IDE project generation.

fan fmake output/hello.fmake -debug -G

The generated project files are located in the build folder under the parent directory.

Guess Number Game

import cstd::*;
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
        var guess = 0;
        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;
}

Built-in Types

  • Int, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64
  • Float, Float32, Float64
  • Bool
  • Void

Int is 32-bit by default, Float is 64-bit by default.

Strings

Strings can span multiple lines:

var s = "ab
            cd";

Triple-quoted strings don't require escaping double quotes:

var s = """ab"
            cd""";

String literals are of type raw*const Int8 and can be automatically converted to sric::String.

Characters

A character represents a single letter and is of type Int8:

var c : Int8 = 'A';

Comments

Single-line comment:

//comment

Multi-line comment:

/*
comment
*/

Documentation comment:

/**
introduction
*/

Annotations

//@method: GET

Annotations can be dynamically accessed through reflection interfaces.

Variable Declaration

  • Use var to declare variables, regardless of mutability.
  • Type annotations come after the variable name.
  • Only one variable can be declared per statement.
var i: Int = 0;

Type inference is only supported for local variables within functions:

var i = 0;

Variables are automatically initialized to default values. Use uninit keyword to keep random values:

var i = uninit;

Global variables must be immutable unless marked with unsafe:

var i: const Int = 0;

Function Definition

  • Functions start with fun
  • Return type can be omitted when it's Void
  • Function names must be unique (no parameter-based overloading)
fun foo(a: Int): Int { return 0; }
fun foo2() {}

Default parameters and named parameters:

fun foo(a: Int, b: Int = 0) {}
foo(a: 1);

Named parameters improve readability by explicitly showing parameter names.

Forward Declaration

There's no forward declaration like in C/C++. Functions can call others defined later in the code because Sric uses a multi-pass compiler architecture.

Visibility

Variables and functions support visibility markers:

public
private
protected
readonly

Examples:

private fun foo() {}
readonly var i: Int = 0;
  • Visibility scope for global variables and functions is the current file. private makes them invisible to other files.
  • Default visibility is public (no need to explicitly specify).
  • protected means visible within the current module or to derived classes.
  • readonly can only modify variables (not functions), meaning public read but private write access.

Value Types

By default, variables in Sric are value types that get automatically copied during assignment/passing. Pointer types only copy the pointer itself, not the pointed-to object.

Pointers

  • Three pointer types: owning, non-owning, and raw
  • Asterisk placement differs from C/C++ (before type)
var p: Int             // Value type
var p: own* Int;       // Owning pointer
var p: * Int;          // Non-owning pointer
var p: & Int;          // Reference
var p: raw* Int;       // Raw pointer
var p: uniq* Int;      // Unique pointer

Sric also provides C++-style smart pointers (SharedPtr, WeakPtr) via standard library.

Memory Allocation

Get pointers via address-of operator or new:

var i: own* Int = new Int;  // Parentheses omitted (no constructors)

Owning Pointers

  • Own their objects (auto-released at scope exit)
  • Require explicit move/copy during transfer:
var p1: own* Int = ...;
var p2 = move p1;          // Transfer ownership
var p3 = share(p1);        // Shared ownership

Unique Pointers

uniq* is zero-overhead. Similar to own*, but without a share() method.

var p1: uniq* Int = makeUniq$<T>();
var p2 = move p1;

Non-owning Pointers

  • No borrowing restrictions like Rust
  • Runtime validity checks
  • Implicit conversion from own*/uniq*:
var p1: own* Int = ...;
var p4: * Int = p1;       // Non-owning
var p5: raw* Int = p1;    // Raw

Raw Pointers

  • C/C++-style pointers (require unsafe):
var p: raw* Int = ...;
unsafe {
    var i = *p;
}

Address-of Operator

Get pointers from values (non-owning by default):

var i: Int = 0;
var p: *Int = &i;

Pointer Arithmetic

Only allowed for raw pointers in unsafe contexts:

var p : raw* Int = ...;
unsafe {
    ++p;
    p = p + 2;
}

References

Similar to C++ but restricted to function parameters/returns:

fun foo(a: & const Int) {}  // Auto-dereferencing

Arrays

Fixed-size arrays (for dynamic arrays see DArray):

var a: [5]Int;                // Explicit size
var a = []Int { 1,3,4 };      // Size inference
constexpr var size = 15;
var a: [size]Int;             // Constexpr size

Null Safety

Pointers are non-nullable by default (use ? for nullable):

var a: own*? B = null;     // Valid
var a: own* B = null;      // Compile error

Immutability

const can modify either pointer or pointee:

var p : raw* const Int;     // Immutable value
var p : const raw* Int;     // Immutable pointer
var p : const raw* const Int;  // Both

Statements

  • All statements end with semicolons
  • No do while or goto (otherwise same as C++)
  • switch doesn't fall through by default (use fallthrough explicitly)
switch (i) {
    case 1:
        fallthrough;
    case 2:
        printf("%d\n", i);
}

Unsafe

Dereference raw pointers in unsafe blocks:

var p: *Int;
...
unsafe {
    var i = *p;
}

Unsafe functions require unsafe blocks:

unsafe fun foo() { ... }

fun main() {
    unsafe {
        foo();
    }
}

Expressions

Operator precedence matches C/C++ except bitwise operators have higher precedence than comparisons

if (i & Mask != 0) {}
// Equivalent to:
if ((i & Mask) != 0) {}

Only prefix ++i is supported (no postfix i++)

With Blocks

With blocks (unlike C++ designated initializers) can contain any statements:

struct A {
    var i: Int;
    fun init() { ... }
}

var a = A { .init(); .i = 0; };
var a: own* A = new A { .i = 0; };

Pointer Access

Use . for both direct and pointer access (no ->):

var a: A;
var b: own* A;
a.foo();
b.foo();

Type Conversion/Checking

as for dynamic/numeric conversion, is for type checking:

var a = p as own* A;
var b = p is own* A;

Other conversions use unsafeCast.

Error Handling

Since Sric does not support exception handling, Optional can serve as an alternative for error return.

Struct

Classes don't have parameterized constructors - callers must initialize them manually:

struct Point {
    var x: Int = 0;
    var y: Int = uninit;
    var z: Int;
}
var p = Point { .y = 1; };

Initialization uses with blocks (different from C's named initialization). These blocks can contain any statements and work in non-initialization contexts:

var point = Point { .y = 1; };
point {
    .x = 2; if (a) { .y = 3; }
}

Methods

  • Types can have methods (non-static member functions with implicit this pointer)
  • Methods must appear after data members
  • Mutability modifier goes after function name:
struct Point {
    fun length() const : Float {
        ...
    }
}

Static Members

Structs can contain static functions/fields (no implicit this):

struct Point {
    static fun foo() {
    }
}
Point::foo();

Inheritance

  • No multiple inheritance (Java-like)
  • Base classes must be marked virtual or abstract
  • Traits (like Java interfaces) can't have data members or method implementations
  • Inheritance list: class first (if any), then traits
  • Overriding requires override marker
  • Use super to call parent methods
virtual struct B {
    virtual fun foo() {}
}
trait I {
    abstract fun foo2();
}
struct A : B , I {
    override fun foo2() {}
}

Constructors/Destructors

Sric has no C++-style constructors - only parameterless default initialization:

struct A {
    fun new() {
    }
    fun delete() {
    }
}

Destructors are rarely needed due to automatic memory management via ownership. Constructors exist mainly to handle complex initialization logic:

struct A {
    var p : own* Int = ...;
    fun new() {
        p = new Int;
    }
}

Type Aliases

Type aliases are equivalent to C's typedef:

typealias size_t = Int32;

Enums

Enums are similar to C++ but always scoped:

enum Color {
    Red, Green = 2, Blue
}

fun foo(c: Color) {}

foo(Color::Red);

Explicit size specification:

enum Color : UInt8 {
    Red, Green = 2, Blue
}

Unsafe Structures

Unsafe structs match their C++ counterparts exactly, without safety check markers. Extern structs are unsafe by default.

Within unsafe structs, this is a raw pointer (not safe pointer). Objects allocated with new can be converted to safe pointers using rawToRef:

unsafe struct A {
    fun foo() {
        var self = rawToRef(this);
    }
}

Generics

Generic Definition

Unlike C++, generics use $< prefix to disambiguate between type parameters and the less-than operator.

struct Tree$<T> {
}

Type parameters can have example types for compile-time type checking:

abstract struct Linkable$<T> {
    var next: own*? T;
    var previous: *? T;
}

struct LinkedList$<T: Linkable$<T>> {
}

Template Instantiation

When instantiating generic templates, any type satisfying the example type constraints can be used:

var tree = Tree$<Int> {};

Closures/Lambdas

Anonymous functions are defined using the fun keyword:

fun foo(f: fun(a:Int):Int) {
    f(1);
}

foo(fun(a:Int):Int{
    printf("%d\n", a);
    return a + 1;
});

Return type inference is not yet supported for closures - the return type must be explicitly specified.

Variable Capture

By default, external variables are captured by value:

var i = 0;
var f = fun(a:Int):Int{
    return a + i;
};

Static Closures

Static closures are state-less closures marked with static. They cannot capture variables:

var f : fun(a:Int) static : Int;

Reference Capture

C++-style reference capture is not supported. For reference capture, you need to explicitly take the address:

var i = 0;
var ri = &i;
var f = fun(a:Int):Int{
    return a + *ri;
};

Move Capture

C++-style move capture is not supported. Use AutoMove to wrap objects and avoid explicit move instructions:

var arr: DArray;
var autoMove = AutoMove { .set(arr); };

var f = fun() {
    var s = autoMove.get().size();
}

Operator Overloading

Use the operator keyword to overload operators:

struct A {
    operator fun mult(a: A): A { ... }
}

var c = a * b;

Overloadable operators:

Methods    Symbols
------     ------
plus       a + b
minus      a - b
mult       a * b
div        a / b
get        a[b]
set        a[b] = c
compare    == != < > <= >=
add        a,b,c;

Comma Operator

The comma operator only works within with blocks:

x { a, b, c; };

This is equivalent to:

x { .add(a).add(b).add(c); }

Modularization

In Sric, modules serve as namespaces, compilation units, and deployment units. Software consists of multiple interdependent modules.

Module Definition

Modules are defined through build scripts with .scm extension:

name = hello
summary = hello
outType = exe
version = 1.0
depends = sric 1.0, cstd 1.0
srcDirs = ./

The source directory srcDirs must end with /. The compiler will automatically search all .sric files in the directory.

Module Import

Import external modules in code:

import sric::*;
import sric::DArray;

Where * imports all symbols under the module. Imported modules must be declared in the depends field of the build script.

Standard Library

Overview

  • sric: Built-in standard library
  • cstd: C standard library wrappers
  • jsonc: JSON parsing/compression
  • serial: Serialization using dynamic reflection

API Documentation

Using C Libraries

The cstd module only exports common C functions - contributions welcome.

To use unexported C functions:

externc fun printf(format: raw* const Int8, args: ...);

fun main() {
    printf("Hello World\n");
}

Declare macros as const variables. See C++ Interop for details.

String

Strings are raw* const Int8 but auto-convert to String:

var str: String = "abc";

Explicit conversion when needed:

var str = asStr("abc");

DArray

Dynamic array (like C++ std::vector):

var a : DArray$<Int>;
a.add(1);
a.add(2);
verify(a[0] == 1);

HashMap

Key-value storage:

var map = HashMap$<Int, String>{};
map.set(1, "1");
map.set(2, "2");
verify(map[2] == "2");

File I/O

Using FileStream:

Write:

var stream = FileStream::open("tmp.txt", "wb");
stream.writeStr("Hello\nWorld");

Read:

var stream = FileStream::open("tmp.txt", "rb");
var line = stream.readAllStr();

Mode strings match C's fopen().

Smart Pointers

Sric provides C++-style smart pointers including SharedPtr, and WeakPtr.

SharedPtr

Reference-counted with some overhead. Can be created from existing own*:

var p = new Int;
var sp: SharedPtr$<Int> = toShared(p);

Convertible with own*:

var p = sharedPtr.getOwn();
sharedPtr.set(p);

WeakPtr

Breaks circular references that could cause memory leaks with own*/SharedPtr:

var p = new Int;
var wp: WeakPtr$<Int> = toWeak(p);

Use via lock() which returns nullable own*:

var sp : own*? Int = wp.lock();

Returns null if referenced object was freed.

Wase

Wase is a cross-platform UI library developed by Sric. With a single codebase, it can be compiled and run on both desktop and web browsers, with future support for Android and iOS development. Wase offers a rich set of controls and elegant interfaces. Support creation UI from configuration file and support custom styles. It employs a self-rendering approach, similar to Qt and Flutter, but is more lightweight. The Sric language has specially designed comma-expression syntax for Wase, enabling a declarative API style comparable to SwiftUI and React Native.

learn more: https://github.com/sric-language/wase

Reflection

Reflection is disabled by default and requires explicit reflect marker:

reflect struct Point {
    var x: Int;
    var y: Int;
}

Annotations can be accessed through reflection API:

//@SimpleSerial
reflect struct Point {
    var x: Int;
    var y: Int;
}

Coroutines

Sric's coroutines are almost identical to JavaScript's. Here's an example:

async fun test2() : Int {
    var i = 0;
    i = await testCallback();
    printf("await result:%d\n", i);
    return i + 1;
}
  • Coroutine functions are marked with async
  • The target of await must be either an async function or a function returning Promise$<T>
  • The return value of async functions is automatically wrapped into Promise$<T> by the compiler

C++ Coroutine Adaptation

Integrating with Event Loop

Sric coroutines are scheduled through the main event loop. The pseudocode shows integration with different event loop implementations:

sric::call_later = [](std::function<void()> h){
    call_in_loop([]{ 
        h();
    });
};

Adapting Asynchronous Callback Interfaces

Allocate sric::Promise<T>::ResultData on heap and call its on_done method in callback:

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);
}

Interoperability with C++

Sric can easily interact with C++. It compiles to human-readable C++ code that can be directly called like regular C++ code.

To call C++ code, simply declare the function prototypes. Use:

  • externc for C-style/no-namespace code
  • extern for matching namespaces
  • Symbol mapping for other cases

C-style/No Namespace

externc fun printf(format: raw* const Int8, args: ...);

fun main() {
    printf("Hello World\n");
}

Matching Namespaces

When C++ namespace matches Sric module name: C++:

namespace xx {
    class P {
        void foo();
    };
}

Sric:

//xx module
extern struct P {
    fun foo();
}

Module name must match C++ namespace.

Symbol Mapping

Use symbol annotation to map symbols: C++:

namespace test {
    void hi() {
    }
}

Sric:

//@extern symbol: test::hi
extern fun hello();

Calling hello() in Sric invokes C++'s hi().

Header Inclusion

Use @#include annotation to include C++ headers:

//@#include "test.h"

Parameterized Constructors

Since Sric doesn't support parameterized constructors, use makePtr/makeValue instead.

Complete Example

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);
}

Generating Sric Interfaces from C++ Headers

Use Python scripts in the tool directory to generate Sric prototypes from C++ headers.

Compiling Without fmake

You can manually compile generated C++ code in sric/output directory.

Define these macros:

  • SC_CHECK: Enable safety checks
  • SC_NO_CHECK: Disable safety checks

If neither is defined, they're automatically set based on _DEBUG/NDEBUG.

Mixed Sric/C++ Compilation

Add fmake configurations in module.scm (prefix with fmake.):

fmake.srcDirs = ./
fmake.incDirs = ./

Serialization

Sric supports serialization through built-in dynamic reflection, using the HiML text format.

Serialization Example

Classes to be serialized require the reflect marker:

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);
}

For non-polymorphic objects like Point, the type name "testSerial::Point" must be explicitly provided.

Skipping Serialization

Use Transient annotation to exclude fields:

reflect struct Point {
    var x: Int;
    //@Transient
    var y: Float;
}

Post-Deserialization Handling

The _onDeserialize method is automatically called after deserialization:

reflect struct Point {
    var x: Int;
    fun _onDeserialize() {
    }
}

Simple Serialization Mode

Normally custom classes serialize as HiML objects. The SimpleSerial annotation enables string serialization (e.g. "1 2 3 4" instead of structured formatInsets{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;
    }
}

Serialization/deserialization automatically calls toString and fromString - these methods must exactly match the shown signatures.

Coding Conventions

Source Files

Source files must use UTF-8 encoding.

Indentation

Use 4 spaces for indentation:

    if (cond) {
        doTrue
    }
    else {
        doFalse
    }

Naming

  • Type names use PascalCase
  • Module names, functions and variables use camelCase
  • Never use ALL_CAPS naming style

Frequently Asked Questions

Unicode Character Encoding Issues

In Git Bash, run:

cmd "/c chcp 65001>nul"

In CMD, run:

chcp 65001>nul

Compiler Doesn't Support C++20

Since coroutines require C++20, you can specify C++ version (note coroutine-related code will fail to compile):

sric module.scm -fmake -c++17

Command Line Tool

Usage

The sric command supports these arguments:

  • -debug Compile in Debug mode (default is release mode)
  • -fmake Use fmake to compile generated C++ code
  • -r Recursively build all dependencies
  • -c++17 Use C++17 standard