Skip to content

Commit

Permalink
added rust binding
Browse files Browse the repository at this point in the history
  • Loading branch information
RyanKung committed Jan 19, 2025
1 parent 4d16f13 commit 267f7b3
Show file tree
Hide file tree
Showing 9 changed files with 441 additions and 41 deletions.
26 changes: 22 additions & 4 deletions dev-cli/container/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ LABEL maintainer="tom wilson <[email protected]>"

# The working directory used by the base image is /src, so we can mount volumes to there
# to expose files on the host to the ao container
#
#
# https://github.com/emscripten-core/emsdk/blob/9b0db91883452051aca8deddc932363aab29060b/docker/Dockerfile#L120

RUN apt-get update --fix-missing -qq -y
Expand Down Expand Up @@ -53,12 +53,12 @@ RUN cp -r /lua-${LUA_VERSION} /lua-${LUA_VERSION}-32
# And, re-compile lua with "generic WASM"
RUN cd /lua-${LUA_VERSION} && \
make clean && \
make generic CC='emcc -s WASM=1 -s MEMORY64=1 -s SUPPORT_LONGJMP=1'
make generic CC='emcc -s WASM=1 -s MEMORY64=1 -s SUPPORT_LONGJMP=1'

# And, re-compile lua with "generic WASM 32-bit"
RUN cd /lua-${LUA_VERSION}-32 && \
make clean && \
make generic CC='emcc -s WASM=1 -s SUPPORT_LONGJMP=1'
make generic CC='emcc -s WASM=1 -s SUPPORT_LONGJMP=1'


#############################
Expand Down Expand Up @@ -115,7 +115,7 @@ ENV NM='/emsdk/upstream/bin/llvm-nm'
###########################################
# We first create a directory for the node impls to be placed
# and dependencies installed
#
#
# By running npm link, we allow any commands exposed by
# the node module to be ran globally within the container
RUN mkdir -p /opt/node
Expand All @@ -124,3 +124,21 @@ RUN cd /opt/node && \
npm install --omit="dev" && \
npm link


###########################################
### Rust and WASM toolchains ###
###########################################

# Set environment variables
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:/usr/local/rustup/bin:$PATH

# Install Rust, components, and cbindgen in a single step
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly && \
rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu && \
cargo install cbindgen && \
rustc --version && cargo --version && cbindgen --version && rustup show

# Make build directory
RUN mkdir /opt/aorustlib
42 changes: 23 additions & 19 deletions dev-cli/container/src/ao-build-module
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def determine_language():
# raise Exception('Multiple languages detected in the module')
if len(langs) == 0:
raise Exception('No language detected in the module')

if langs.count('lua') == 0 and langs.count('c') >= 1:
lang = 'c'
elif langs.count('lua') == 0 and langs.count('rust') >= 1:
Expand All @@ -51,7 +51,7 @@ def determine_language():
def main():
# Load the definition file
definition = Definition('/opt/definition.yml')

# Load the config file
config = Config('/src/config.yml')

Expand All @@ -67,26 +67,26 @@ def main():
c_source_files = []
injected_lua_files = []
dependency_libraries = []

# Inject c files into c_program if language is c
if(language == 'c'):
c_program = inject_c_files(definition, c_program, c_source_files, link_libraries)

# Inject rust files into c_program if language is rust
if(language == 'rust'):
c_program = inject_rust_files(definition, c_program)

# Inject lua files into c_program always to load lua files and replace placeholders
if(language == 'lua'):
c_program = inject_lua_files(definition, c_program, injected_lua_files)

# Load all libraries
c_program = load_libraries(config, definition, c_program, link_libraries, c_source_files, injected_lua_files,dependency_libraries)

# Inject the lua files into the c program
c_program = c_program.replace(
'__INJECT_LUA_FILES__', '\n'.join(injected_lua_files))

#Generate compile target file
debug_print('Start to generate complie.c')

Expand All @@ -97,25 +97,25 @@ def main():
debug_print('Start to compile as WASM')

# Setup the compile command
cmd = ['emcc', '-O3',
cmd = ['emcc', '-O3',
'-g2',
# '-s', 'MAIN_MODULE', # This is required to load dynamic libraries at runtime
'-s', 'ASYNCIFY=1',
'-s', 'STACK_SIZE=' + str(config.stack_size),
'-s', 'ASYNCIFY_STACK_SIZE=' + str(config.stack_size),
'-s', 'ALLOW_MEMORY_GROWTH=1',
'-s', 'INITIAL_MEMORY=' + str(config.initial_memory),
'-s', 'MAXIMUM_MEMORY=' + str(config.maximum_memory),
'-s', 'WASM=1',
'-s', 'ALLOW_MEMORY_GROWTH=1',
'-s', 'INITIAL_MEMORY=' + str(config.initial_memory),
'-s', 'MAXIMUM_MEMORY=' + str(config.maximum_memory),
'-s', 'WASM=1',
'-s', 'MODULARIZE',
# '-s', 'FILESYSTEM=0',
'-s', 'DETERMINISTIC=1',
'-s', 'NODERAWFS=0',
# '-s', 'FILESYSTEM=0',
'-s', 'DETERMINISTIC=1',
'-s', 'NODERAWFS=0',
'-s', 'FORCE_FILESYSTEM=1',
'-msimd128',
'--pre-js', '/opt/pre.js'
]

# If the target is 64 bit, add the MEMORY64 flag and link against 64 bit libraries
if config.target == 64:
cmd.extend(['-sMEMORY64=1'])
Expand All @@ -128,7 +128,7 @@ def main():

# Link Rust library
if(language == 'rust'):
cmd.extend(['-L/opt/aorustlib', '-l:libaorust.a'])
cmd.extend(['-I/opt/aorustlib', '-L/opt/aorustlib', '-laorust'])

cmd.extend(['-s','ASSERTIONS=1'])
cmd.extend(definition.get_extra_args())
Expand All @@ -138,7 +138,7 @@ def main():
# Add all c source files if there are any
for f in c_source_files:
cmd.append(quote(f))

# Add the compile file and link libraries
cmd.extend(['/tmp/compile.c'])
cmd.extend([quote(v.filepath) for v in link_libraries])
Expand All @@ -157,8 +157,12 @@ def main():
# add metering library
# meter_cmd = ['node', '/opt/node/apply-metering.cjs']
# shell_exec(*meter_cmd)

if config.keep_js is False:
shell_exec(*['rm', os.path.join(os.getcwd(), 'process.js')])
process_js_path = os.path.join(os.getcwd(), 'process.js')
# For rust case, js code is not exists
if os.path.exists(process_js_path):
shell_exec(*['rm', process_js_path])

if __name__ == '__main__':
main()
Expand Down
40 changes: 22 additions & 18 deletions dev-cli/container/src/ao_module_lib/languages/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,34 @@
def inject_rust_files(definition: Definition, c_program: str):

c_header_files = []
rust_source_files = []
# rust_source_files = []

c_header_files += glob.glob('/src/**/*.h', recursive=True)
c_header_files += glob.glob('/src/**/*.h', recursive=True)
c_header_files += glob.glob('/src/**/*.hpp', recursive=True)

inc = []
for header in c_header_files:
c_program = '#include "{}"\n'.format(header) + c_program

# c_program = 'const char *process_handle(const char *arg_0, const char *arg_1);\n' + c_program
c_program = 'const char *process_handle(const char *arg_0, const char *arg_1);\n' + c_program


c_program = c_program.replace('__FUNCTION_DECLARATIONS__', definition.make_c_function_delarations())
c_program = c_program.replace('__LUA_BASE__', "")
c_program = c_program.replace('__LUA_MAIN__', "")


rust_source_files += glob.glob('/src/**/*.rs', recursive=True)
rust_src_dir = os.path.join(RUST_DIR, "src")
# rust_source_files += glob.glob('/src/**/*.rs', recursive=True)
# rust_src_dir = os.path.join(RUST_DIR, "src")

for file in rust_source_files:
if os.path.isfile(file):
shutil.copy2(file, os.path.join(rust_src_dir))
# for file in rust_source_files:
# if os.path.isfile(file):
# shutil.copy2(file, os.path.join(rust_src_dir))

prev_dir = os.getcwd()
os.chdir(RUST_DIR)
# prev_dir = os.getcwd()
# os.chdir(RUST_DIR)

# build rust code at '/src'
os.environ["RUSTFLAGS"] = "--cfg=web_sys_unstable_apis --Z wasm_c_abi=spec"
cmd = [
'cargo',
Expand All @@ -46,19 +49,20 @@ def inject_rust_files(definition: Definition, c_program: str):
]

shell_exec(*cmd)
rust_lib = glob.glob('/opt/aorustlib/**/release/*.a', recursive=True)[0]
rust_lib = glob.glob('/src/target/wasm64-unknown-unknown/release/*.a', recursive=True)[0]
rust_lib = shutil.copy2(rust_lib, RUST_DIR)

cmd = [
'cbindgen',
'--config',
'cbindgen',
'--config',
'cbindgen.toml',
'--crate',
'aorust',
'--output',
'--crate',
'aorust',
'--output',
'aorust.h'
]
shell_exec(*cmd)
c_program = '#include "{}"\n'.format('/opt/aorustlib/aorust.h') + c_program
os.chdir(prev_dir)
rust_header = shutil.copy2('/src/aorust.h', RUST_DIR)
c_program = '#include "{}"\n'.format('aorust.h') + c_program
# os.chdir(prev_dir)
return c_program
11 changes: 11 additions & 0 deletions dev-cli/src/starters/rust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/target/
Cargo.lock
**/*.log
**/*.tmp
*.swp
*.bak
*.bk
*.tmp
*.old
aorust.h
process.wasm
12 changes: 12 additions & 0 deletions dev-cli/src/starters/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "aorust"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["staticlib"]

[dependencies]
serde = "1.0"
serde_json = "1.0"

15 changes: 15 additions & 0 deletions dev-cli/src/starters/rust/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
all:build bindgen

build:
RUSTFLAGS="--cfg=web_sys_unstable_apis --Z wasm_c_abi=spec" \
cargo +nightly build \
-Zbuild-std=std,panic_unwind,panic_abort \
--target=wasm64-unknown-unknown \
--release

bindgen:
cbindgen --config cbindgen.toml --crate aorust --output aorust.h

clean:
cargo clean
rm aorust.h
7 changes: 7 additions & 0 deletions dev-cli/src/starters/rust/cbindgen.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
language = "C"

[defines]
MY_DEFINE = "1"

[export]
include = ["process_handler"]
72 changes: 72 additions & 0 deletions dev-cli/src/starters/rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
pub mod libao;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

/// Processes the input `msg` and `env` strings, performs some operations,
/// and returns the result as a C-compatible string.
///
/// # Safety
/// - The caller must ensure that the `msg` and `env` pointers are valid, null-terminated C strings.
/// - The caller is responsible for freeing the returned string using `free_c_string`.
///
/// # Arguments
/// - `msg`: A pointer to a null-terminated C string representing the message.
/// - `env`: A pointer to a null-terminated C string representing the environment.
///
/// # Returns
/// - A pointer to a null-terminated C string containing the result. The caller must not modify
/// or attempt to free this pointer.
#[no_mangle]
pub extern "C" fn process_handle(msg: *const c_char, env: *const c_char) -> *const c_char {
// Convert the C strings to Rust strings
let msg = unsafe { CStr::from_ptr(msg).to_str().unwrap_or("Invalid msg") };
let env = unsafe { CStr::from_ptr(env).to_str().unwrap_or("Invalid env") };

// Call the Rust handler function to process the inputs
let ret = handler(msg, env);

// Convert the Rust string result into a CString
let c_response = CString::new(ret).unwrap();

// Return the raw pointer to the C-compatible string
// The caller is now responsible for freeing this memory
c_response.into_raw()
}

pub fn handler(msg: &str, env: &str) -> String {
let mut ao = libao::AO::new();
ao.init(env);
ao.log("Normalize");
let norm = ao.normalize(msg);
ao.log(&msg);
let send = &ao.send(&norm);
println!("Send: {}", send);
"{\"ok\": true,\"response\":{\"Output\":\"Success\"},\"Memory\":50000000}".to_string()
}

#[cfg(test)]
mod tests {
use super::*;
use std::ffi::{CStr, CString};

#[test]
fn test_process_handle() {
let msg_json = r#"{"key": "value"}"#;
let env_json = r#"{"env": "test"}"#;

let c_msg = CString::new(msg_json).expect("Failed to create CString for msg");
let c_env = CString::new(env_json).expect("Failed to create CString for env");

let c_result = process_handle(c_msg.as_ptr(), c_env.as_ptr());

assert!(!c_result.is_null(), "Returned pointer is null");

let result_str = unsafe { CStr::from_ptr(c_result).to_string_lossy().into_owned() };

let expected_result = r#"{"ok": true,"response":{"Output":"Success"},"Memory":50000000}"#;
assert_eq!(
result_str, expected_result,
"Output does not match expected result"
);
}
}
Loading

0 comments on commit 267f7b3

Please sign in to comment.