From a10d07ccbf3f3f0567b3f7b0e4394f3d903c4957 Mon Sep 17 00:00:00 2001 From: ihciah Date: Thu, 7 Nov 2024 15:14:59 +0000 Subject: [PATCH] feat: support go-side object pool --- README.md | 10 ++ docs/cli-args.md | 37 +++++++ examples/example-monoio/build.rs | 1 + examples/example-monoio/go/gen.go | 75 ++++++++++++- rust2go-cli/src/lib.rs | 13 ++- rust2go-common/src/raw_file.rs | 178 +++++++++++++++++++++++++++--- 6 files changed, 294 insertions(+), 20 deletions(-) create mode 100644 docs/cli-args.md diff --git a/README.md b/README.md index 1c091ad..e2e8592 100644 --- a/README.md +++ b/README.md @@ -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` and `String` is represented as a pointer and a length. However, structs like `Vec` or `Vec>` 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 @@ -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). diff --git a/docs/cli-args.md b/docs/cli-args.md new file mode 100644 index 0000000..3ce2507 --- /dev/null +++ b/docs/cli-args.md @@ -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(); +} +``` diff --git a/examples/example-monoio/build.rs b/examples/example-monoio/build.rs index 853198e..797aca1 100644 --- a/examples/example-monoio/build.rs +++ b/examples/example-monoio/build.rs @@ -7,6 +7,7 @@ fn main() { src: "./src/user.rs".into(), dst: "./go/gen.go".into(), go118: true, + recycle: true, ..Default::default() }) .build(); diff --git a/examples/example-monoio/go/gen.go b/examples/example-monoio/go/gen.go index 2bd1375..25283e5 100644 --- a/examples/example-monoio/go/gen.go +++ b/examples/example-monoio/go/gen.go @@ -66,6 +66,7 @@ import "C" import ( "reflect" "runtime" + "sync" "unsafe" ) @@ -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 @@ -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) }() } @@ -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) }() } @@ -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) @@ -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 @@ -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{} } @@ -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{} @@ -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{} } diff --git a/rust2go-cli/src/lib.rs b/rust2go-cli/src/lib.rs index e8e4e9b..a533979 100644 --- a/rust2go-cli/src/lib.rs +++ b/rust2go-cli/src/lib.rs @@ -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) { @@ -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 { diff --git a/rust2go-common/src/raw_file.rs b/rust2go-common/src/raw_file.rs index 2c8c049..46eed8f 100644 --- a/rust2go-common/src/raw_file.rs +++ b/rust2go-common/src/raw_file.rs @@ -205,6 +205,7 @@ typedef struct QueueMeta { &self, levels: &HashMap, go118: bool, + recycle: bool, ) -> Result { const GO118CODE: &str = r#" // An alternative impl of unsafe.String for go1.18 @@ -244,12 +245,7 @@ typedef struct QueueMeta { } "#; - let mut out = if go118 { - GO118CODE.to_string() - } else { - GO121CODE.to_string() - } + r#" - func cntString(_ *string, _ *uint) [0]C.StringRef { return [0]C.StringRef{} } + const NON_RECYCLE_LIST: &str = r#" 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) @@ -260,6 +256,52 @@ typedef struct QueueMeta { return output } } + "#; + const RECYCLE_LIST: &str = r#" + 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) + + // 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 + } + } + "#; + + let mut out = if go118 { + GO118CODE.to_string() + } else { + GO121CODE.to_string() + }; + + if !recycle { + out += NON_RECYCLE_LIST; + } else { + out += RECYCLE_LIST; + } + + out += r#" + 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) @@ -383,6 +425,52 @@ typedef struct QueueMeta { func refC_float(p *float32, _ *[]byte) C.float { return C.float(*p) } func refC_double(p *float64, _ *[]byte) C.double { return C.double(*p) } "#; + if recycle { + out.push_str( + r#" + 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), + } + "#, + ); + } for item in self.file.items.iter() { match item { // for example, convert @@ -425,14 +513,63 @@ typedef struct QueueMeta { out.push_str(&format!( "func new{struct_name}(p C.{struct_name}Ref) {struct_name}{{\nreturn {struct_name}{{\n" )); + let mut name_types = Vec::new(); for field in s.fields.iter() { let field_name = field.ident.as_ref().unwrap().to_string(); let field_type = ParamType::try_from(&field.ty)?; - let (new_f, _) = field_type.c_to_go_field_converter(levels); + let (new_f, f_level) = field_type.c_to_go_field_converter(levels); out.push_str(&format!("{field_name}: {new_f}(p.{field_name}),\n",)); + name_types.push((field_name, field_type, f_level)); } out.push_str("}\n}\n"); + // recStruct + if recycle { + out.push_str(&format!( + "func rec{struct_name}(s *{struct_name}, p *_GenericPool){{\n" + )); + for (field_name, field_type, f_level) in name_types { + if f_level >= 2 { + match field_type.inner { + ParamTypeInner::Custom(_) => { + out.push_str(&format!("p.Put(s.{field_name})\n")); + } + ParamTypeInner::List(inner) => { + let seg = type_to_segment(&inner).unwrap(); + let inside = match &seg.arguments { + syn::PathArguments::AngleBracketed(ga) => { + match ga.args.last().unwrap() { + syn::GenericArgument::Type(ty) => ty, + _ => panic!("list generic must be a type"), + } + } + _ => { + panic!( + "list type must have angle bracketed arguments" + ) + } + }; + let pt = ParamType::try_from(inside) + .expect("unable to convert list type"); + let (_, inner_level) = pt.c_to_go_field_converter(levels); + if inner_level >= 2 { + out.push_str(&format!( + "for i := range s.{field_name} {{\n", + )); + out.push_str(&format!( + "rec{}(&s.{field_name}[i], p)\n", + pt.to_go() + )); + } + out.push_str(&format!("p.Put(s.{field_name})\n")); + } + _ => {} + } + } + } + out.push_str("}\n"); + } + let level = *levels.get(&s.ident).unwrap(); // cntStruct @@ -1070,12 +1207,12 @@ impl TraitRepr { } // Generate golang exports. - pub fn generate_go_exports(&self, levels: &HashMap) -> String { + pub fn generate_go_exports(&self, levels: &HashMap, recycle: bool) -> String { let name = self.name.to_string(); let mut out: String = self .fns .iter() - .map(|f| f.to_go_export(&name, levels)) + .map(|f| f.to_go_export(&name, levels, recycle)) .collect(); let shm_cnt = self.fns.iter().filter(|f| f.mem_call_id.is_some()).count(); if shm_cnt != 0 { @@ -1237,7 +1374,7 @@ inline void {fn_name}_cb(const void *f_ptr, {c_resp_type} resp, const void *slot } } - fn to_go_export(&self, trait_name: &str, levels: &HashMap) -> String { + fn to_go_export(&self, trait_name: &str, levels: &HashMap, recycle: bool) -> String { if let Some(mem_call_id) = self.mem_call_id { let fn_sig = format!("func ringHandle{trait_name}{mem_call_id}(ptr unsafe.Pointer, pool *ants.MultiPool, post_func func(interface{{}}, []byte, uint)) {{\n"); let Some(ret) = &self.ret else { @@ -1288,12 +1425,17 @@ inline void {fn_name}_cb(const void *f_ptr, {c_resp_type} resp, const void *slot let mut new_names = Vec::new(); let mut new_cvt = String::new(); + let mut rec_req_lines = String::new(); let ref_mark = BoolMark::new(self.go_ptr, "&"); for p in self.params.iter() { let new_name = format_ident!("_new_{}", p.name); - let cvt = p.ty.c_to_go_field_converter(levels).0; + let (cvt, lv) = p.ty.c_to_go_field_converter(levels); new_cvt.push_str(&format!("{new_name} := {cvt}({})\n", p.name)); - new_names.push(format!("{ref_mark}{}", new_name)); + if recycle && lv >= 2 { + rec_req_lines + .push_str(&format!("rec{}(&{new_name}, _GLOBAL_POOL)\n", p.ty.to_go())); + } + new_names.push(format!("{ref_mark}{new_name}")); } match (self.is_async, &self.ret) { (true, None) => panic!("async function must have a return value"), @@ -1309,6 +1451,7 @@ inline void {fn_name}_cb(const void *f_ptr, {c_resp_type} resp, const void *slot fn_name = self.name, params = new_names.join(", ") )); + out.push_str(&rec_req_lines); out.push_str("}\n"); } (false, Some(ret)) => { @@ -1338,6 +1481,7 @@ inline void {fn_name}_cb(const void *f_ptr, {c_resp_type} resp, const void *slot "C.{callback}(unsafe.Pointer(cb), resp_ref, unsafe.Pointer(slot))\n", )); out.push_str("runtime.KeepAlive(resp)\nruntime.KeepAlive(buffer)\n"); + out.push_str(&rec_req_lines); out.push_str("}\n"); } (true, Some(ret)) => { @@ -1371,6 +1515,7 @@ inline void {fn_name}_cb(const void *f_ptr, {c_resp_type} resp, const void *slot "C.{callback}(unsafe.Pointer(cb), resp_ref, unsafe.Pointer(slot))\n", )); out.push_str("runtime.KeepAlive(resp)\nruntime.KeepAlive(buffer)\n"); + out.push_str(&rec_req_lines); out.push_str("}()\n}\n"); } } @@ -1739,11 +1884,16 @@ mod tests { println!( "structs gen: {}", - raw_file.convert_structs_to_go(&levels, false).unwrap() + raw_file + .convert_structs_to_go(&levels, false, true) + .unwrap() ); for trait_ in traits { println!("if gen: {}", trait_.generate_go_interface()); - println!("go export gen: {}", trait_.generate_go_exports(&levels)); + println!( + "go export gen: {}", + trait_.generate_go_exports(&levels, true) + ); } let levels = raw_file.convert_structs_levels().unwrap(); levels.iter().for_each(|f| println!("{}: {}", f.0, f.1));