diff --git a/.gitignore b/.gitignore index 7b093f9e1a3..77521831c5e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ swift/wallet-core/ codegen-v2/bindings/ src/Generated/*.cpp +include/Generated/TrustWalletCore/*.h include/TrustWalletCore/TWHRP.h include/TrustWalletCore/TW*Proto.h include/TrustWalletCore/TWEthereumChainID.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6fec5513326..63c1444f57b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,7 @@ endif () target_include_directories(TrustWalletCore PUBLIC $ + $ $ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src diff --git a/codegen-v2/Cargo.lock b/codegen-v2/Cargo.lock index 6ed11b1c48a..098922255d8 100644 --- a/codegen-v2/Cargo.lock +++ b/codegen-v2/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -35,6 +35,7 @@ dependencies = [ "handlebars", "heck", "pathdiff", + "regex", "serde", "serde_json", "serde_yaml", @@ -214,22 +215,51 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "ryu" version = "1.0.17" @@ -293,9 +323,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", diff --git a/codegen-v2/Cargo.toml b/codegen-v2/Cargo.toml index 43c2afea1c6..b1692860f20 100644 --- a/codegen-v2/Cargo.toml +++ b/codegen-v2/Cargo.toml @@ -21,3 +21,4 @@ serde_yaml = "0.9.21" toml_edit = "0.21.0" handlebars = "4.3.6" heck = "0.4.1" +regex = "1.11.1" diff --git a/codegen-v2/src/codegen/cpp/code_gen.rs b/codegen-v2/src/codegen/cpp/code_gen.rs new file mode 100644 index 00000000000..a73f1d89a9e --- /dev/null +++ b/codegen-v2/src/codegen/cpp/code_gen.rs @@ -0,0 +1,302 @@ +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::fmt::Write as _; +use std::fs; +use std::io::Write; + +use crate::Error::BadFormat; +use crate::Result; + +static IN_DIR: &str = "../rust/bindings/"; +static HEADER_OUT_DIR: &str = "../include/Generated/TrustWalletCore/"; +static SOURCE_OUT_DIR: &str = "../src/Generated/"; + +#[derive(Deserialize, Serialize, Debug)] +pub struct TWConfig { + pub class: String, + pub static_functions: Vec, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct TWStaticFunction { + pub name: String, + pub rust_name: String, + pub args: Vec, + pub return_type: String, + pub docs: Vec, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct TWArg { + pub name: String, + pub ty: String, +} + +fn convert_rust_type_to_cpp(ty: &str) -> String { + let trimmed = ty.replace(" ", ""); + if let Some(captures) = Regex::new(r"^Nonnull<(.+)>$") + .expect("Failed to create regex") + .captures(&trimmed) + { + format!("{} *_Nonnull", &captures[1]) + } else if let Some(captures) = Regex::new(r"^NonnullMut<(.+)>$") + .expect("Failed to create regex") + .captures(&trimmed) + { + format!("{} *_Nonnull", &captures[1]) + } else if let Some(captures) = Regex::new(r"^Nullable<(.+)>$") + .expect("Failed to create regex") + .captures(&trimmed) + { + format!("{} *_Nullable", &captures[1]) + } else if let Some(captures) = Regex::new(r"^NullableMut<(.+)>$") + .expect("Failed to create regex") + .captures(&trimmed) + { + format!("{} *_Nullable", &captures[1]) + } else { + ty.to_string() + } +} + +fn generate_license(file: &mut std::fs::File) -> Result<()> { + writeln!(file, "// SPDX-License-Identifier: Apache-2.0")?; + writeln!(file, "//")?; + writeln!(file, "// Copyright © 2017 Trust Wallet.\n")?; + Ok(()) +} + +fn generate_header_guard(file: &mut std::fs::File) -> Result<()> { + writeln!(file, "#pragma once\n")?; + Ok(()) +} + +fn generate_header_includes(file: &mut std::fs::File, info: &TWConfig) -> Result<()> { + writeln!(file, "#include ")?; + + // Include headers based on argument types + let mut included_headers = std::collections::HashSet::new(); + for func in &info.static_functions { + for arg in &func.args { + if arg.ty.contains("TWString") && included_headers.insert("TWString.h") { + writeln!(file, "#include ")?; + } + // Additional type checks can be added here in the future + } + } + + Ok(()) +} + +fn generate_class_declaration(file: &mut std::fs::File, info: &TWConfig) -> Result<()> { + writeln!(file, "TW_EXPORT_CLASS\nstruct {};\n", info.class)?; + Ok(()) +} + +fn generate_function_signature( + class_name: &str, + func: &TWStaticFunction, + is_declaration: bool, +) -> Result { + let return_type = convert_rust_type_to_cpp(&func.return_type); + let whether_export = if is_declaration { + "TW_EXPORT_STATIC_METHOD " + } else { + "" + }; + let mut signature = format!("{whether_export}{return_type} {class_name}{}", func.name); + signature += "("; + for (i, arg) in func.args.iter().enumerate() { + write!( + &mut signature, + "{} {}", + convert_rust_type_to_cpp(&arg.ty), + arg.name + ) + .map_err(|e| BadFormat(e.to_string()))?; + if i < func.args.len() - 1 { + signature += ", "; + } + } + signature += ")"; + Ok(signature) +} + +fn generate_function_declaration( + file: &mut std::fs::File, + class_name: &str, + func: &TWStaticFunction, +) -> Result<()> { + let func_dec = generate_function_signature(class_name, func, true)?; + for doc in &func.docs { + writeln!(file, "/// {}", doc)?; + } + writeln!(file, "{func_dec};\n")?; + Ok(()) +} + +pub fn generate_header(info: &TWConfig) -> Result<()> { + let file_path = format!("{HEADER_OUT_DIR}/{}.h", info.class); + let mut file = std::fs::File::create(&file_path)?; + + generate_license(&mut file)?; + generate_header_guard(&mut file)?; + generate_header_includes(&mut file, info)?; + + writeln!(file, "\nTW_EXTERN_C_BEGIN\n")?; + + generate_class_declaration(&mut file, info)?; + for func in &info.static_functions { + generate_function_declaration(&mut file, &info.class, func)?; + } + + writeln!(file, "TW_EXTERN_C_END")?; + + file.flush()?; + + Ok(()) +} + +fn generate_source_includes(file: &mut std::fs::File, info: &TWConfig) -> Result<()> { + writeln!( + file, + "#include ", + info.class + )?; + writeln!(file, "#include \"rust/Wrapper.h\"")?; + Ok(()) +} + +fn generate_function_call(args: &Vec) -> Result { + let mut func_call = "(".to_string(); + for (i, arg) in args.iter().enumerate() { + write!(&mut func_call, "{arg}").map_err(|e| BadFormat(e.to_string()))?; + if i < args.len() - 1 { + func_call += ", "; + } + } + func_call += ");\n"; + Ok(func_call) +} + +fn generate_return_type(func: &TWStaticFunction, converted_args: &Vec) -> Result { + let mut return_string = String::new(); + match func.return_type.replace(" ", "").as_str() { + "NullableMut" | "Nullable" => { + write!( + &mut return_string, + " const Rust::TWStringWrapper result = Rust::{}", + func.rust_name + ) + .map_err(|e| BadFormat(e.to_string()))?; + return_string += generate_function_call(&converted_args)?.as_str(); + writeln!(&mut return_string, " if (!result) {{ return nullptr; }}") + .map_err(|e| BadFormat(e.to_string()))?; + writeln!( + &mut return_string, + " return TWStringCreateWithUTF8Bytes(result.c_str());" + ) + .map_err(|e| BadFormat(e.to_string()))?; + } + _ => { + writeln!(&mut return_string, " return Rust::{}", func.rust_name) + .map_err(|e| BadFormat(e.to_string()))?; + return_string += generate_function_call(&converted_args)?.as_str(); + } + } + Ok(return_string) +} + +fn generate_conversion_code_with_var_name(ty: &str, name: &str) -> Result<(String, String)> { + match ty { + "TWString *_Nonnull" => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tauto& {name}String = *reinterpret_cast({name});\n\ + \tconst Rust::TWStringWrapper {name}RustStr = {name}String;" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}RustStr.get()", name))) + } + "TWString *_Nullable" => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tconst TW::Rust::TWString* {name}Ptr;\n\ + \tif ({name} != nullptr) {{\n\ + \t\tauto& {name}String = *reinterpret_cast({name});\n\ + \t\tconst Rust::TWStringWrapper {name}RustStr = {name}String;\n\ + \t\t{name}Ptr = {name}RustStr.get();\n\ + \t}} else {{\n\ + \t\t{name}Ptr = nullptr;\n\ + \t}}" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}Ptr", name))) + } + _ => Ok(("".to_string(), name.to_string())), + } +} + +fn generate_function_definition( + file: &mut std::fs::File, + info: &TWConfig, + func: &TWStaticFunction, +) -> Result<()> { + let mut func_def = generate_function_signature(&info.class, func, false)?; + func_def += " {\n"; + let mut converted_args = vec![]; + for arg in func.args.iter() { + let func_type = convert_rust_type_to_cpp(&arg.ty); + let (conversion_code, converted_arg) = + generate_conversion_code_with_var_name(&func_type, &arg.name)?; + func_def += conversion_code.as_str(); + converted_args.push(converted_arg); + } + let return_string = generate_return_type(func, &converted_args)?; + func_def += return_string.as_str(); + func_def += "}\n"; + writeln!(file, "{}", func_def)?; + Ok(()) +} + +fn generate_source(info: &TWConfig) -> Result<()> { + let file_path = format!("{SOURCE_OUT_DIR}/{}.cpp", info.class); + let mut file = std::fs::File::create(&file_path)?; + + generate_license(&mut file)?; + generate_source_includes(&mut file, info)?; + + writeln!(file, "\nusing namespace TW;\n")?; + + for func in &info.static_functions { + generate_function_definition(&mut file, info, func)?; + } + + file.flush()?; + + Ok(()) +} + +pub fn generate_cpp_bindings() -> Result<()> { + std::fs::create_dir_all(HEADER_OUT_DIR)?; + std::fs::create_dir_all(SOURCE_OUT_DIR)?; + + let entries = fs::read_dir(IN_DIR)?; + for entry in entries { + let file_path = entry?.path(); + if file_path.is_dir() { + println!("Found unexpected directory: {}", file_path.display()); + continue; + } + + let file_contents = fs::read_to_string(&file_path)?; + let info: TWConfig = + serde_yaml::from_str(&file_contents).expect("Failed to parse YAML file"); + + generate_header(&info)?; + generate_source(&info)?; + } + Ok(()) +} diff --git a/codegen-v2/src/codegen/cpp/mod.rs b/codegen-v2/src/codegen/cpp/mod.rs index 1ef7188d86f..147960dd30c 100644 --- a/codegen-v2/src/codegen/cpp/mod.rs +++ b/codegen-v2/src/codegen/cpp/mod.rs @@ -7,6 +7,7 @@ use std::env; use std::path::PathBuf; pub mod blockchain_dispatcher_generator; +pub mod code_gen; pub mod entry_generator; pub mod new_blockchain; pub mod new_cosmos_chain; diff --git a/codegen-v2/src/codegen/template_generator.rs b/codegen-v2/src/codegen/template_generator.rs index 7b725ba74e7..6fae696716d 100644 --- a/codegen-v2/src/codegen/template_generator.rs +++ b/codegen-v2/src/codegen/template_generator.rs @@ -38,7 +38,14 @@ impl TemplateGenerator { .add_pattern("{TW_CRATE_NAME}", coin.id.to_tw_crate_name()) .add_pattern("{COIN_ID}", coin.id.as_str()) .add_pattern("{COIN_TYPE}", coin.coin_type()) - .add_pattern("{COIN_NAME}", if coin.display_name.len() > 0 { &coin.display_name } else { &coin.name }) + .add_pattern( + "{COIN_NAME}", + if coin.display_name.len() > 0 { + &coin.display_name + } else { + &coin.name + }, + ) .add_pattern("{SYMBOL}", &coin.symbol) .add_pattern("{DECIMALS}", coin.decimals) .add_pattern("{P2PKH_PREFIX}", coin.p2pkh_prefix) diff --git a/codegen-v2/src/main.rs b/codegen-v2/src/main.rs index 53fd140cc65..f420701bd6c 100644 --- a/codegen-v2/src/main.rs +++ b/codegen-v2/src/main.rs @@ -2,6 +2,7 @@ // // Copyright © 2017 Trust Wallet. +use libparser::codegen::cpp::code_gen::generate_cpp_bindings; use libparser::codegen::swift::RenderIntput; use libparser::codegen::{cpp, proto, rust}; use libparser::coin_id::CoinId; @@ -23,6 +24,7 @@ fn main() -> Result<()> { "new-evmchain" => new_evmchain(&args[2..]), "new-cosmos-chain" => new_cosmos_chain(&args[2..]), "swift" => generate_swift_bindings(), + "cpp" => generate_cpp_bindings(), _ => Err(Error::InvalidCommand), } } diff --git a/codegen/bin/codegen b/codegen/bin/codegen index 5cd99179bb8..1438a5e73a2 100755 --- a/codegen/bin/codegen +++ b/codegen/bin/codegen @@ -15,6 +15,7 @@ Encoding.default_internal = Encoding::UTF_8 options = OpenStruct.new # default input / output path options.input = "#{CurrentDir}/../../include/TrustWalletCore" +options.input_generated = "#{CurrentDir}/../../include/Generated/TrustWalletCore" options.output = "#{CurrentDir}/../../" options.swift = true options.java = true @@ -62,14 +63,17 @@ end.parse! entities = [] files = [] -Dir.foreach(options.input) do |item| - next if ['.', '..', '.DS_Store'].include?(item) +[options.input, options.input_generated].each do |input_dir| + Dir.foreach(input_dir) do |item| + next if ['.', '..', '.DS_Store'].include?(item) - entity = Parser.new(path: File.expand_path(File.join(options.input, item))).parse - next if entity.nil? + file_path = File.expand_path(File.join(input_dir, item)) + entity = Parser.new(path: file_path).parse + next if entity.nil? - entities << entity - files << File.basename(item, '.h').sub(/^TW/, '') + entities << entity + files << File.basename(item, '.h').sub(/^TW/, '') + end end generator = CodeGenerator.new(entities: entities, files: files, output_folder: options.output) diff --git a/include/TrustWalletCore/TWTONAddressConverter.h b/include/TrustWalletCore/TWTONAddressConverter.h deleted file mode 100644 index 39bb4dfed7e..00000000000 --- a/include/TrustWalletCore/TWTONAddressConverter.h +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "TWBase.h" -#include "TWString.h" - -TW_EXTERN_C_BEGIN - -/// TON address operations. -TW_EXPORT_CLASS -struct TWTONAddressConverter; - -/// Converts a TON user address into a Bag of Cells (BoC) with a single root Cell. -/// The function is mostly used to request a Jetton user address via `get_wallet_address` RPC. -/// https://docs.ton.org/develop/dapps/asset-processing/jettons#retrieving-jetton-wallet-addresses-for-a-given-user -/// -/// \param address Address to be converted into a Bag Of Cells (BoC). -/// \return Pointer to a base64 encoded Bag Of Cells (BoC). Null if invalid address provided. -TW_EXPORT_STATIC_METHOD -TWString *_Nullable TWTONAddressConverterToBoc(TWString *_Nonnull address); - -/// Parses a TON address from a Bag of Cells (BoC) with a single root Cell. -/// The function is mostly used to parse a Jetton user address received on `get_wallet_address` RPC. -/// https://docs.ton.org/develop/dapps/asset-processing/jettons#retrieving-jetton-wallet-addresses-for-a-given-user -/// -/// \param boc Base64 encoded Bag Of Cells (BoC). -/// \return Pointer to a Jetton address. -TW_EXPORT_STATIC_METHOD -TWString *_Nullable TWTONAddressConverterFromBoc(TWString *_Nonnull boc); - -/// Converts any TON address format to user friendly with the given parameters. -/// -/// \param address raw or user-friendly address to be converted. -/// \param bounceable whether the result address should be bounceable. -/// \param testnet whether the result address should be testnet. -/// \return user-friendly address str. -TW_EXPORT_STATIC_METHOD -TWString *_Nullable TWTONAddressConverterToUserFriendly(TWString *_Nonnull address, bool bounceable, bool testnet); - -TW_EXTERN_C_END diff --git a/kotlin/wallet-core-kotlin/build.gradle.kts b/kotlin/wallet-core-kotlin/build.gradle.kts index 8fc8be0a32f..b04858f3aa1 100644 --- a/kotlin/wallet-core-kotlin/build.gradle.kts +++ b/kotlin/wallet-core-kotlin/build.gradle.kts @@ -100,7 +100,13 @@ kotlin { val main by compilations.getting main.cinterops.create("WalletCore") { packageName = "com.trustwallet.core" - headers(rootDir.parentFile.resolve("include/TrustWalletCore").listFiles()!!) + includeDirs( + rootDir.parentFile.resolve("include"), + rootDir.parentFile.resolve("include/TrustWalletCore"), + rootDir.parentFile.resolve("include/Generated/TrustWalletCore") + ) + headers(rootDir.parentFile.resolve("include/Generated/TrustWalletCore").listFiles()!! + + rootDir.parentFile.resolve("include/TrustWalletCore").listFiles()!!) } } } diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml new file mode 100644 index 00000000000..47684146c24 --- /dev/null +++ b/rust/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +CARGO_WORKSPACE_DIR = { value = "", relative = true } diff --git a/rust/.gitignore b/rust/.gitignore index bca6f9b85aa..d963b2c89d3 100644 --- a/rust/.gitignore +++ b/rust/.gitignore @@ -5,3 +5,6 @@ target/ # These are backup files generated by rustfmt **/*.rs.bk + +# Generated by tw_macros +bindings/ diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 6f1b6ee3db7..a3cb2b29049 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -314,7 +314,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.96", "syn_derive", ] @@ -531,7 +531,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.96", ] [[package]] @@ -567,6 +567,17 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "derive_arbitrary" version = "1.3.0" @@ -750,9 +761,9 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -839,9 +850,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", @@ -1199,9 +1210,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -1223,9 +1234,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -1411,9 +1422,9 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.189" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -1429,13 +1440,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.96", ] [[package]] @@ -1457,7 +1468,20 @@ checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.96", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", ] [[package]] @@ -1540,7 +1564,7 @@ checksum = "e6dc88f1f470d9de1001ffbb90d2344c9dd1a615f5467daf0574e2975dfd9ebd" dependencies = [ "starknet-curve", "starknet-ff", - "syn 2.0.37", + "syn 2.0.96", ] [[package]] @@ -1594,7 +1618,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.37", + "syn 2.0.96", ] [[package]] @@ -1616,9 +1640,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.37" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -1634,7 +1658,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.96", ] [[package]] @@ -2050,6 +2074,18 @@ dependencies = [ "tw_zcash", ] +[[package]] +name = "tw_macros" +version = "0.1.0" +dependencies = [ + "derive-syn-parse", + "proc-macro2", + "quote", + "serde", + "serde_yaml", + "syn 2.0.96", +] + [[package]] name = "tw_memory" version = "0.1.0" @@ -2370,6 +2406,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "uuid" version = "1.7.0" @@ -2403,6 +2445,7 @@ dependencies = [ "tw_ethereum", "tw_hash", "tw_keypair", + "tw_macros", "tw_memory", "tw_misc", "tw_number", @@ -2553,5 +2596,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.96", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index f51f38dde9d..d84739eb539 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -33,6 +33,7 @@ members = [ "tw_evm", "tw_hash", "tw_keypair", + "tw_macros", "tw_memory", "tw_misc", "tw_number", diff --git a/rust/tw_macros/Cargo.toml b/rust/tw_macros/Cargo.toml new file mode 100644 index 00000000000..8a0343c15be --- /dev/null +++ b/rust/tw_macros/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tw_macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +derive-syn-parse = "0.2.0" +proc-macro2 = "1.0.93" +quote = "1.0.38" +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9.21" +syn = { version = "2.0.96", features = ["full"] } diff --git a/rust/tw_macros/src/code_gen.rs b/rust/tw_macros/src/code_gen.rs new file mode 100644 index 00000000000..3b311486b9b --- /dev/null +++ b/rust/tw_macros/src/code_gen.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug)] +pub struct TWConfig { + pub class: String, + pub static_functions: Vec, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct TWStaticFunction { + pub name: String, + pub rust_name: String, + pub args: Vec, + pub return_type: String, + pub docs: Vec, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct TWArg { + pub name: String, + pub ty: String, +} diff --git a/rust/tw_macros/src/lib.rs b/rust/tw_macros/src/lib.rs new file mode 100644 index 00000000000..8c0cd9533ed --- /dev/null +++ b/rust/tw_macros/src/lib.rs @@ -0,0 +1,12 @@ +use proc_macro::TokenStream; + +mod code_gen; +mod tw_ffi; + +#[proc_macro_attribute] +pub fn tw_ffi(attr: TokenStream, item: TokenStream) -> TokenStream { + match tw_ffi::tw_ffi(attr.into(), item.into()) { + Ok(item) => item.into(), + Err(e) => e.to_compile_error().into(), + } +} diff --git a/rust/tw_macros/src/tw_ffi.rs b/rust/tw_macros/src/tw_ffi.rs new file mode 100644 index 00000000000..fcf1c804f91 --- /dev/null +++ b/rust/tw_macros/src/tw_ffi.rs @@ -0,0 +1,179 @@ +use derive_syn_parse::Parse; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + parse2, Ident, Result, Token, +}; + +use std::env; +use std::fmt; +use std::fs; +use std::path::Path; + +use crate::code_gen::{TWArg, TWConfig, TWStaticFunction}; + +pub mod keywords { + use syn::custom_keyword; + + custom_keyword!(ty); + custom_keyword!(class); + custom_keyword!(name); + custom_keyword!(static_function); +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TWFFIType { + StaticFunction, +} + +impl Parse for TWFFIType { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(keywords::static_function) { + input.parse::()?; + Ok(Self::StaticFunction) + } else { + Err(lookahead.error()) + } + } +} + +impl fmt::Display for TWFFIType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TWFFIType::StaticFunction => write!(f, "static_function"), + } + } +} + +#[derive(Parse, Clone)] +pub struct TWFFIAttrArgs { + pub _ty_keyword: Option, + #[parse_if(_ty_keyword.is_some())] + pub _eq: Option, + #[parse_if(_ty_keyword.is_some())] + pub _ty: Option, + #[parse_if(_ty_keyword.is_some())] + pub _comma: Option, + + pub _class_keyword: Option, + #[parse_if(_class_keyword.is_some())] + pub _eq2: Option, + #[parse_if(_class_keyword.is_some())] + pub class: Option, + #[parse_if(_class_keyword.is_some())] + pub _comma2: Option, + + pub _name_keyword: Option, + #[parse_if(_name_keyword.is_some())] + pub _eq3: Option, + #[parse_if(_name_keyword.is_some())] + pub name: Option, +} + +pub fn tw_ffi(attr: TokenStream2, item: TokenStream2) -> Result { + let args = parse2::(attr)?; + + let func = parse2::(item.clone())?; + let func_name = func.sig.ident.to_string(); + let func_args = func + .sig + .inputs + .iter() + .map(|arg| match arg { + syn::FnArg::Typed(syn::PatType { pat, ty, .. }) => (quote!( #pat ), quote!( #ty )), + _ => (quote!(), quote!()), + }) + .map(|(name, ty)| TWArg { + name: name.to_string(), + ty: ty.to_string(), + }) + .collect::>(); + + let return_type = func.sig.output; + let return_type = match return_type { + syn::ReturnType::Type(_, ty) => quote!(#ty).to_string(), + _ => "void".to_string(), + }; + + let class = args.class.unwrap().to_string(); + let docs = func + .attrs + .iter() + .flat_map(|attr| { + if let syn::Meta::NameValue(meta) = &attr.meta { + if meta.path.is_ident("doc") { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = &meta.value + { + return Some(lit.value().trim().to_string()); + } + } + } + None + }) + .collect::>(); + let static_function = TWStaticFunction { + name: args.name.unwrap().to_string(), + rust_name: func_name, + args: func_args, + return_type, + docs, + }; + + if let Ok(out_dir) = env::var("CARGO_WORKSPACE_DIR") { + let bindings_dir = Path::new(&out_dir).join("bindings"); + fs::create_dir_all(&bindings_dir).expect("Failed to create bindings directory"); + let yaml_file_path = bindings_dir.join(format!("{}.yaml", class)); + + let mut config = if yaml_file_path.exists() { + serde_yaml::from_str( + &fs::read_to_string(&yaml_file_path).expect("Failed to read existing YAML file"), + ) + .expect("Failed to parse YAML file") + } else { + TWConfig { + class, + static_functions: vec![], + } + }; + if let Some(idx) = config + .static_functions + .iter() + .position(|f| f.name == static_function.name) + { + config.static_functions[idx] = static_function; + } else { + config.static_functions.push(static_function); + } + + let yaml_output: String = + serde_yaml::to_string(&config).expect("Failed to serialize to YAML"); + fs::write(&yaml_file_path, yaml_output).expect("Failed to write YAML file"); + } else { + panic!("CARGO_WORKSPACE_DIR is not set"); + } + + Ok(item) +} + +#[cfg(test)] +mod tests { + use super::*; + use proc_macro2::Span; + + #[test] + fn test_ffi_attr_arg_parsing() { + let args = parse2::(quote! { + ty = static_function, + class = MyClass, + name = MyName + }) + .unwrap(); + assert_eq!(args.class, Some(Ident::new("MyClass", Span::call_site()))); + assert_eq!(args.name, Some(Ident::new("MyName", Span::call_site()))); + } +} diff --git a/rust/tw_memory/src/ffi/mod.rs b/rust/tw_memory/src/ffi/mod.rs index c69f1550a6a..0ee0b6becba 100644 --- a/rust/tw_memory/src/ffi/mod.rs +++ b/rust/tw_memory/src/ffi/mod.rs @@ -54,3 +54,8 @@ pub trait RawPtrTrait: Sized { Some(Box::from_raw(raw)) } } + +pub type Nullable = *const T; +pub type Nonnull = *const T; +pub type NullableMut = *mut T; +pub type NonnullMut = *mut T; diff --git a/rust/wallet_core_rs/Cargo.toml b/rust/wallet_core_rs/Cargo.toml index 1b8f34f853a..c66a335b140 100644 --- a/rust/wallet_core_rs/Cargo.toml +++ b/rust/wallet_core_rs/Cargo.toml @@ -43,6 +43,7 @@ tw_hash = { path = "../tw_hash", optional = true } tw_keypair = { path = "../tw_keypair", optional = true } tw_memory = { path = "../tw_memory", optional = true } tw_number = { path = "../tw_number", optional = true } +tw_macros = { path = "../tw_macros" } tw_misc = { path = "../tw_misc" } tw_proto = { path = "../tw_proto", optional = true } tw_solana = { path = "../chains/tw_solana", optional = true } diff --git a/rust/wallet_core_rs/src/ffi/ton/address_converter.rs b/rust/wallet_core_rs/src/ffi/ton/address_converter.rs index f16ee89ac69..e8e1fe57699 100644 --- a/rust/wallet_core_rs/src/ffi/ton/address_converter.rs +++ b/rust/wallet_core_rs/src/ffi/ton/address_converter.rs @@ -5,8 +5,9 @@ #![allow(clippy::missing_safety_doc)] use std::str::FromStr; +use tw_macros::tw_ffi; use tw_memory::ffi::tw_string::TWString; -use tw_memory::ffi::RawPtrTrait; +use tw_memory::ffi::{Nonnull, NullableMut, RawPtrTrait}; use tw_misc::try_or_else; use tw_ton::address::TonAddress; use tw_ton::modules::address_converter::AddressConverter; @@ -17,10 +18,11 @@ use tw_ton::modules::address_converter::AddressConverter; /// /// \param address Address to be converted into a Bag Of Cells (BoC). /// \return Pointer to a base64 encoded Bag Of Cells (BoC). Null if invalid address provided. +#[tw_ffi(ty = static_function, class = TWTONAddressConverter, name = ToBoc)] #[no_mangle] pub unsafe extern "C" fn tw_ton_address_converter_to_boc( - address: *const TWString, -) -> *mut TWString { + address: Nonnull, +) -> NullableMut { let address = try_or_else!(TWString::from_ptr_as_ref(address), std::ptr::null_mut); let address_str = try_or_else!(address.as_str(), std::ptr::null_mut); let address_ton = try_or_else!(TonAddress::from_str(address_str), std::ptr::null_mut); @@ -39,8 +41,11 @@ pub unsafe extern "C" fn tw_ton_address_converter_to_boc( /// /// \param boc Base64 encoded Bag Of Cells (BoC). /// \return Pointer to a Jetton address. +#[tw_ffi(ty = static_function, class = TWTONAddressConverter, name = FromBoc)] #[no_mangle] -pub unsafe extern "C" fn tw_ton_address_converter_from_boc(boc: *const TWString) -> *mut TWString { +pub unsafe extern "C" fn tw_ton_address_converter_from_boc( + boc: Nonnull, +) -> NullableMut { let boc = try_or_else!(TWString::from_ptr_as_ref(boc), std::ptr::null_mut); let boc_str = try_or_else!(boc.as_str(), std::ptr::null_mut); @@ -58,12 +63,13 @@ pub unsafe extern "C" fn tw_ton_address_converter_from_boc(boc: *const TWString) /// \param bounceable whether the result address should be bounceable. /// \param testnet whether the result address should be testnet. /// \return user-friendly address str. +#[tw_ffi(ty = static_function, class = TWTONAddressConverter, name = ToUserFriendly)] #[no_mangle] pub unsafe extern "C" fn tw_ton_address_converter_to_user_friendly( - address: *const TWString, + address: Nonnull, bounceable: bool, testnet: bool, -) -> *mut TWString { +) -> NullableMut { let address = try_or_else!(TWString::from_ptr_as_ref(address), std::ptr::null_mut); let address_str = try_or_else!(address.as_str(), std::ptr::null_mut); let address_ton = try_or_else!(TonAddress::from_str(address_str), std::ptr::null_mut) diff --git a/src/interface/TWTONAddressConverter.cpp b/src/interface/TWTONAddressConverter.cpp deleted file mode 100644 index 4113448ad24..00000000000 --- a/src/interface/TWTONAddressConverter.cpp +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include - -#include "Base64.h" -#include "rust/Wrapper.h" - -using namespace TW; - -TWString *_Nullable TWTONAddressConverterToBoc(TWString *_Nonnull address) { - auto& addressString = *reinterpret_cast(address); - - const Rust::TWStringWrapper addressRustStr = addressString; - const Rust::TWStringWrapper bocRustStr = Rust::tw_ton_address_converter_to_boc(addressRustStr.get()); - if (!bocRustStr) { - return nullptr; - } - - return TWStringCreateWithUTF8Bytes(bocRustStr.c_str()); -} - -TWString *_Nullable TWTONAddressConverterFromBoc(TWString *_Nonnull boc) { - auto& bocEncoded = *reinterpret_cast(boc); - - const Rust::TWStringWrapper bocRustStr = bocEncoded; - const Rust::TWStringWrapper addressRustStr = Rust::tw_ton_address_converter_from_boc(bocRustStr.get()); - if (!addressRustStr) { - return nullptr; - } - - return TWStringCreateWithUTF8Bytes(addressRustStr.c_str()); -} - -TWString *_Nullable TWTONAddressConverterToUserFriendly(TWString *_Nonnull address, bool bounceable, bool testnet) { - auto& addressString = *reinterpret_cast(address); - - const Rust::TWStringWrapper addressRustStr = addressString; - const Rust::TWStringWrapper userFriendlyRustStr = Rust::tw_ton_address_converter_to_user_friendly(addressRustStr.get(), bounceable, testnet); - if (!userFriendlyRustStr) { - return nullptr; - } - - return TWStringCreateWithUTF8Bytes(userFriendlyRustStr.c_str()); -} diff --git a/tools/generate-files b/tools/generate-files index de3abc7f906..a987d9c19f6 100755 --- a/tools/generate-files +++ b/tools/generate-files @@ -56,15 +56,15 @@ mkdir -p swift/Sources/Generated/Protobuf swift/Sources/Generated/Enums # Generate coins info from registry.json codegen/bin/coins +# Generate rust bindgen +tools/rust-bindgen "$@" + # Generate interface code, Swift bindings excluded. codegen/bin/codegen # Convert doxygen comments to appropriate format tools/doxygen_convert_comments -# Generate rust bindgen -tools/rust-bindgen "$@" - # Generate Java, C++ and Swift Protobuf files if [ -x "$(command -v protoc-gen-swift)" ] && isTargetSpecified "ios"; then diff --git a/tools/rust-bindgen b/tools/rust-bindgen index 0edaf9c23a5..2ed44523398 100755 --- a/tools/rust-bindgen +++ b/tools/rust-bindgen @@ -72,6 +72,9 @@ cbindgen --crate $CRATE --output ../src/rust/bindgen/$HEADER_NAME cd - [[ -e rust/target/release/${TARGET_NAME} ]] && cp rust/target/release/${TARGET_NAME} build/local/lib/ +echo "Generating C++ files..." +pushd codegen-v2 && cargo run -- cpp && popd + if isTargetSpecified "ios" && [[ $(uname) == "Darwin" ]]; then cd rust cat > $TARGET_XCFRAMEWORK_NAME/Info.plist << EOF @@ -146,3 +149,4 @@ cat > $TARGET_XCFRAMEWORK_NAME/Info.plist << EOF EOF cd - fi +