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:
- Reducing features and complexity.
- 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 |
---|---|
int | Int |
short | Int16 |
int32_t | Int32 |
unsigned int | UInt32 |
int64_t | Int64 |
uint64_t | UInt64 |
float | Float32 |
double | Float/Float64 |
void | Void |
char | Int8 |
Defines
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 |
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
- JDK 17+
- C++ compiler (supporting C++20): gcc 11+, clang 17+, Xcode 16+, Visual Studio 2022+
- Fanx
- CMake
- git
- VSCode
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 -
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
IDE
- Search 'sric-language' in vscode marketplace, install it.
- 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
-
Create an empty folder as the workspace
-
Create a file named main.sric with the following content:
import cstd::*;
fun main(): Int {
printf("Hello World\n");
return 0;
}
- 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 = ./
- Build
sric module.scm -fmake
build debug mode:
sric module.scm -fmake -debug
- 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
orgoto
(otherwise same as C++) switch
doesn't fall through by default (usefallthrough
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
orabstract
- 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 librarycstd
: C standard library wrappersjsonc
: JSON parsing/compressionserial
: Serialization using dynamic reflection
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 codeextern
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