Skip to content

Commit

Permalink
feat: support go-side object pool
Browse files Browse the repository at this point in the history
  • Loading branch information
ihciah committed Nov 7, 2024
1 parent c29dba0 commit a10d07c
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 20 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,26 @@ For detailed example, please checkout [the example projects](./examples).
> Detailed design details can be found in this article: [Design and Implementation of a Rust-Go FFI Framework](https://en.ihcblog.com/rust2go/).
### Why Fast?

In order to achieve the ultimate performance, this project is not based on communication, but on FFI to pass specially encoded data. In order to reduce memory operations to a minimum, data that satisfies a specific memory layout is passed directly by reference rather than copied.

For example, `Vec<u8>` and `String` is represented as a pointer and a length. However, structs like `Vec<String>` or `Vec<Vec<u8>>` require intermediate representation. In order to reduce the number of memory allocations to one, I use a precomputed size buffer to store these intermediate structures.

### Memory Safety

On the Golang side, the data it receives is referenced from Rust. The Rust side will do its best to ensure the validity of this data during the call. So the Golang side can implement the handler arbitrarily, but manually deep copy when leaking data outside the function life cycle.

On the Rust side, it is needed to ensure that the slot pointer of the callback ffi operation, and the user parameters are valid when the future drops. This is archieved by implementing an atomic slot structure and providing a `[drop_safe]` attribute to require user passing parameters with ownership.

## Toolchain Requirements

- Golang: >=1.18
- For >=1.18 && < 1.20: generate golang code with `--go118`
- For >=1.20: generate golang code normally
- Rust: >=1.75 if you want to use async

With my experience, starting from Golang 1.21 there is a significant performance improvement in CGO. So I recommend using Golang 1.21 or later.

## Milestones
### Init Version
- [x] IDL(in rust) parse
Expand All @@ -58,9 +63,14 @@ On the Rust side, it is needed to ensure that the slot pointer of the callback f

### Performance Optimization
- [x] Shared memory based implementation
- [x] Go-side reference passing support saving stack grow cost for big size data
- [x] Go-side object pool support saving allocation cost for complicated data types

### Extended Features
- [ ] Support calling rust from golang

### Exploratory Features
- [ ] Support sense peer memory layout at boot time or compile time and access fields directly

## Credit
This project is inspired by [fcplug](https://github.com/andeya/fcplug).
37 changes: 37 additions & 0 deletions docs/cli-args.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: Commandline tool arguments
date: 2024-11-07 00:00:00
author: ihciah
---

# Commandline Tool Arguments
1. `src` \[required\]: Path of source rust file
2. `dst` \[required\]: Path of destination go file
3. `without_main` \[optional, default=`false`\]: With or without go main function
4. `go118` \[optional, default=`false`\]: Go 1.18 compatible
5. `no_fmt` \[optional, default=`false`\]: Disable auto format go file
6. `recycle` \[optional, default=`false`\]: Enable object pool

# Usage
1. The arguments can be used in commline tool:
```shell
rust2go-cli --src src.rs --dst dst.go --without_main --go118 --no_fmt --recycle
```

2. The arguments can also be used in `build.rs` to generate go file automatically:
```rust
use rust2go::RegenArgs;

fn main() {
rust2go::Builder::new()
.with_go_src("./go")
.with_regen_arg(RegenArgs {
src: "./src/user.rs".into(),
dst: "./go/gen.go".into(),
go118: true,
recycle: true,
..Default::default()
})
.build();
}
```
1 change: 1 addition & 0 deletions examples/example-monoio/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ fn main() {
src: "./src/user.rs".into(),
dst: "./go/gen.go".into(),
go118: true,
recycle: true,
..Default::default()
})
.build();
Expand Down
75 changes: 73 additions & 2 deletions examples/example-monoio/go/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import "C"
import (
"reflect"
"runtime"
"sync"
"unsafe"
)

Expand Down Expand Up @@ -94,6 +95,7 @@ func CDemoCall_demo_check(req C.DemoComplicatedRequestRef, slot *C.void, cb *C.v
C.DemoCall_demo_check_cb(unsafe.Pointer(cb), resp_ref, unsafe.Pointer(slot))
runtime.KeepAlive(resp)
runtime.KeepAlive(buffer)
recDemoComplicatedRequest(&_new_req, _GLOBAL_POOL)
}

//export CDemoCall_demo_check_async
Expand All @@ -105,6 +107,7 @@ func CDemoCall_demo_check_async(req C.DemoComplicatedRequestRef, slot *C.void, c
C.DemoCall_demo_check_async_cb(unsafe.Pointer(cb), resp_ref, unsafe.Pointer(slot))
runtime.KeepAlive(resp)
runtime.KeepAlive(buffer)
recDemoComplicatedRequest(&_new_req, _GLOBAL_POOL)
}()
}

Expand All @@ -117,6 +120,7 @@ func CDemoCall_demo_check_async_safe(req C.DemoComplicatedRequestRef, slot *C.vo
C.DemoCall_demo_check_async_safe_cb(unsafe.Pointer(cb), resp_ref, unsafe.Pointer(slot))
runtime.KeepAlive(resp)
runtime.KeepAlive(buffer)
recDemoComplicatedRequest(&_new_req, _GLOBAL_POOL)
}()
}

Expand Down Expand Up @@ -164,17 +168,36 @@ func refString(s *string, _ *[]byte) C.StringRef {
}
}

func cntString(_ *string, _ *uint) [0]C.StringRef { return [0]C.StringRef{} }
func new_list_mapper[T1, T2 any](f func(T1) T2) func(C.ListRef) []T2 {
return func(x C.ListRef) []T2 {
input := unsafe.Slice((*T1)(unsafe.Pointer(x.ptr)), x.len)
output := make([]T2, len(input))

// try to get from _GLOBAL_POOL
elem := _GLOBAL_POOL.Get(reflect.TypeOf([]T2{}))
var output []T2
if elem != nil {
output = elem.([]T2)
if cap(output) < len(input) {
// if the capacity is not enough, create a new one
// old one will not be used anymore
output = make([]T2, len(input))
} else {
// if the capacity is enough, truncate the slice
output = output[:len(input)]
}
} else {
// if not found in _GLOBAL_POOL, create a new one
output = make([]T2, len(input))
}

for i, v := range input {
output[i] = f(v)
}
return output
}
}

func cntString(_ *string, _ *uint) [0]C.StringRef { return [0]C.StringRef{} }
func new_list_mapper_primitive[T1, T2 any](_ func(T1) T2) func(C.ListRef) []T2 {
return func(x C.ListRef) []T2 {
return unsafe.Slice((*T2)(unsafe.Pointer(x.ptr)), x.len)
Expand Down Expand Up @@ -301,6 +324,47 @@ func refC_intptr_t(p *int, _ *[]byte) C.intptr_t { return C.intptr_t(*p) }
func refC_float(p *float32, _ *[]byte) C.float { return C.float(*p) }
func refC_double(p *float64, _ *[]byte) C.double { return C.double(*p) }

type _GenericPool struct {
mapping map[reflect.Type]*sync.Pool
mu sync.RWMutex
}

func (p *_GenericPool) Get(typ reflect.Type) interface{} {
p.mu.RLock()
pool, ok := p.mapping[typ]
p.mu.RUnlock()
if !ok {
return nil
}
return pool.Get()
}

// x: []T
func (p *_GenericPool) Put(x interface{}) {
// check if x is []T
typ := reflect.TypeOf(x)
if typ.Kind() != reflect.Slice {
return
}

p.mu.RLock()
pool, ok := p.mapping[typ]
p.mu.RUnlock()
if !ok {
pool = &sync.Pool{}
p.mu.Lock()
if _, ok := p.mapping[typ]; !ok {
p.mapping[typ] = pool
}
p.mu.Unlock()
}
pool.Put(x)
}

var _GLOBAL_POOL = &_GenericPool{
mapping: make(map[reflect.Type]*sync.Pool),
}

type DemoUser struct {
name string
age uint8
Expand All @@ -312,6 +376,8 @@ func newDemoUser(p C.DemoUserRef) DemoUser {
age: newC_uint8_t(p.age),
}
}
func recDemoUser(s *DemoUser, p *_GenericPool) {
}
func cntDemoUser(s *DemoUser, cnt *uint) [0]C.DemoUserRef {
return [0]C.DemoUserRef{}
}
Expand All @@ -333,6 +399,9 @@ func newDemoComplicatedRequest(p C.DemoComplicatedRequestRef) DemoComplicatedReq
balabala: new_list_mapper_primitive(newC_uint8_t)(p.balabala),
}
}
func recDemoComplicatedRequest(s *DemoComplicatedRequest, p *_GenericPool) {
p.Put(s.users)
}
func cntDemoComplicatedRequest(s *DemoComplicatedRequest, cnt *uint) [0]C.DemoComplicatedRequestRef {
cnt_list_mapper(cntDemoUser)(&s.users, cnt)
return [0]C.DemoComplicatedRequestRef{}
Expand All @@ -353,6 +422,8 @@ func newDemoResponse(p C.DemoResponseRef) DemoResponse {
pass: newC_bool(p.pass),
}
}
func recDemoResponse(s *DemoResponse, p *_GenericPool) {
}
func cntDemoResponse(s *DemoResponse, cnt *uint) [0]C.DemoResponseRef {
return [0]C.DemoResponseRef{}
}
Expand Down
13 changes: 9 additions & 4 deletions rust2go-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub struct Args {
/// Disable auto format go file
#[arg(long, default_value = "false")]
pub no_fmt: bool,

/// Enable object pool
#[arg(long, default_value = "false")]
pub recycle: bool,
}

pub fn generate(args: &Args) {
Expand Down Expand Up @@ -77,18 +81,19 @@ pub fn generate(args: &Args) {
};
let import_cgo = if use_cgo { "\"runtime\"\n" } else { "" };

let import_118 = if args.go118 { "\"reflect\"\n" } else { "" };
let import_reflect = if args.go118 { "\"reflect\"\n" } else { "" };
let import_sync = if args.recycle { "\"sync\"\n" } else { "" };
let mut go_content = format!(
"package main\n\n/*\n{output}*/\nimport \"C\"\nimport (\n\"unsafe\"\n{import_cgo}{import_118}{import_shm})\n"
"package main\n\n/*\n{output}*/\nimport \"C\"\nimport (\n\"unsafe\"\n{import_cgo}{import_sync}{import_reflect}{import_shm})\n"
);
let levels = raw_file.convert_structs_levels().unwrap();
traits.iter().for_each(|t| {
go_content.push_str(&t.generate_go_interface());
go_content.push_str(&t.generate_go_exports(&levels));
go_content.push_str(&t.generate_go_exports(&levels, args.recycle));
});
go_content.push_str(
&raw_file
.convert_structs_to_go(&levels, args.go118)
.convert_structs_to_go(&levels, args.go118, args.recycle)
.expect("Unable to generate go structs"),
);
if use_shm {
Expand Down
Loading

0 comments on commit a10d07c

Please sign in to comment.