Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions cargo-near-build/src/near/abi/generate/dylib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ use std::collections::HashSet;
use std::fs;

use camino::Utf8Path;
use eyre::WrapErr;

#[cfg(unix)]
use std::process::Command;

#[cfg(unix)]
use crate::cargo_native::ArtifactType;
use crate::cargo_native::Dylib;
use crate::pretty_print;
use crate::types::near::build::output::CompilationArtifact;
Expand Down Expand Up @@ -36,6 +42,13 @@ pub fn extract_abi_entries(
pretty_print::indent_payload(&format!("{:#?}", &near_abi_symbols))
);

// User-authored #[no_mangle] functions can reference NEAR host imports.
// Those symbols are unresolved on host and would make dlopen fail before
// we can call __near_abi_* export functions. Load a small shim library
// with no-op definitions so ABI extraction can proceed.
#[cfg(unix)]
let _host_function_stubs = load_near_host_function_stubs()?;

let mut entries = vec![];
unsafe {
let lib = libloading::Library::new(dylib_path.as_str())?;
Expand Down Expand Up @@ -67,3 +80,156 @@ pub fn extract_abi_entries(
}
Ok(entries)
}

#[cfg(unix)]
struct LoadedHostFunctionStubs {
_temp_dir: tempfile::TempDir,
_library: libloading::os::unix::Library,
}

#[cfg(unix)]
fn load_near_host_function_stubs() -> eyre::Result<LoadedHostFunctionStubs> {
use libloading::os::unix::{Library, RTLD_GLOBAL, RTLD_LAZY};

let temp_dir = tempfile::Builder::new()
.prefix("cargo-near-abi-host-stubs")
.tempdir()?;
let source_path = temp_dir.path().join("near_host_stubs.rs");
let library_path = temp_dir.path().join(format!(
"libnear_host_stubs.{}",
<Dylib as ArtifactType>::extension()
));

fs::write(&source_path, near_host_stubs_source())?;

let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
let output = Command::new(&rustc)
.arg("--crate-name")
.arg("near_host_stubs")
.arg("--crate-type")
.arg("cdylib")
.arg("--edition=2021")
.arg(&source_path)
.arg("-o")
.arg(&library_path)
.output()
.wrap_err_with(|| format!("failed to execute `{rustc}` while compiling ABI host stubs"))?;

if !output.status.success() {
eyre::bail!(
"failed to compile ABI host stubs with `{}`:\n{}",
rustc,
String::from_utf8_lossy(&output.stderr)
);
}

let library = unsafe { Library::open(Some(library_path.as_os_str()), RTLD_LAZY | RTLD_GLOBAL) }
.wrap_err_with(|| {
format!(
"failed to load ABI host stubs from `{}`",
library_path.display()
)
})?;

Ok(LoadedHostFunctionStubs {
_temp_dir: temp_dir,
_library: library,
})
}

#[cfg(unix)]
fn near_host_stubs_source() -> String {
let mut source = String::from("#![allow(non_snake_case)]\n\n");
for function in NEAR_HOST_FUNCTIONS {
source.push_str("#[unsafe(no_mangle)]\n");
source.push_str(&format!(
"pub unsafe extern \"C\" fn {function}() -> u64 {{ 0 }}\n\n"
));
}
source
}

#[cfg(unix)]
const NEAR_HOST_FUNCTIONS: &[&str] = &[
"read_register",
"register_len",
"write_register",
"current_account_id",
"current_contract_code",
"refund_to_account_id",
"signer_account_id",
"signer_account_pk",
"predecessor_account_id",
"input",
"block_index",
"block_timestamp",
"epoch_height",
"storage_usage",
"account_balance",
"account_locked_balance",
"attached_deposit",
"prepaid_gas",
"used_gas",
"random_seed",
"sha256",
"keccak256",
"keccak512",
"ripemd160",
"ecrecover",
"ed25519_verify",
"value_return",
"panic",
"panic_utf8",
"log_utf8",
"log_utf16",
"abort",
"promise_create",
"promise_then",
"promise_and",
"promise_batch_create",
"promise_batch_then",
"promise_set_refund_to",
"promise_batch_action_state_init",
"promise_batch_action_state_init_by_account_id",
"set_state_init_data_entry",
"promise_batch_action_create_account",
"promise_batch_action_deploy_contract",
"promise_batch_action_function_call",
"promise_batch_action_function_call_weight",
"promise_batch_action_transfer",
"promise_batch_action_stake",
"promise_batch_action_add_key_with_full_access",
"promise_batch_action_add_key_with_function_call",
"promise_batch_action_delete_key",
"promise_batch_action_delete_account",
"promise_batch_action_deploy_global_contract",
"promise_batch_action_deploy_global_contract_by_account_id",
"promise_batch_action_use_global_contract",
"promise_batch_action_use_global_contract_by_account_id",
"promise_yield_create",
"promise_yield_resume",
"promise_results_count",
"promise_result",
"promise_return",
"storage_write",
"storage_read",
"storage_remove",
"storage_has_key",
"storage_iter_prefix",
"storage_iter_range",
"storage_iter_next",
"validator_stake",
"validator_total_stake",
"alt_bn128_g1_multiexp",
"alt_bn128_g1_sum",
"alt_bn128_pairing_check",
"bls12381_p1_sum",
"bls12381_p2_sum",
"bls12381_g1_multiexp",
"bls12381_g2_multiexp",
"bls12381_map_fp_to_g1",
"bls12381_map_fp2_to_g2",
"bls12381_pairing_check",
"bls12381_p1_decompress",
"bls12381_p2_decompress",
];
30 changes: 26 additions & 4 deletions cargo-near-build/src/near/abi/generate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,28 @@ pub fn procedure(

pretty_print::step("Generating ABI");

let compile_env = {
let compile_env = vec![
let (compile_env, hide_warnings_for_compile) = {
let mut compile_env = vec![
("CARGO_PROFILE_DEV_OPT_LEVEL", "0"),
("CARGO_PROFILE_DEV_DEBUG", "0"),
("CARGO_PROFILE_DEV_LTO", "off"),
(env_keys::BUILD_RS_ABI_STEP_HINT, "true"),
];
[&compile_env, env].concat()
compile_env.extend_from_slice(env);

let mut hide_warnings_for_compile = hide_warnings;
if let Some(rustflags) = abi_generation_rustflags(hide_warnings) {
compile_env.push((env_keys::RUSTFLAGS, rustflags));
hide_warnings_for_compile = false;
}

(compile_env, hide_warnings_for_compile)
};
let dylib_artifact = cargo_native::compile::run::<Dylib>(
&crate_metadata.manifest_path,
cargo_args.as_slice(),
compile_env,
hide_warnings,
hide_warnings_for_compile,
color,
)?;

Expand Down Expand Up @@ -125,3 +133,17 @@ fn strip_docs(abi_root: &mut near_abi::AbiRoot) {
}
}
}

#[cfg(target_os = "macos")]
fn abi_generation_rustflags(hide_warnings: bool) -> Option<&'static str> {
if hide_warnings {
Some("-Awarnings -C link-arg=-undefined -C link-arg=dynamic_lookup")
} else {
Some("-C link-arg=-undefined -C link-arg=dynamic_lookup")
}
}

#[cfg(not(target_os = "macos"))]
fn abi_generation_rustflags(_hide_warnings: bool) -> Option<&'static str> {
None
}
41 changes: 40 additions & 1 deletion integration-tests/tests/abi/e2e.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use cargo_near_build::near_abi::{
AbiFunction, AbiFunctionKind, AbiJsonParameter, AbiParameters, AbiType,
};
use cargo_near_integration_tests::generate_abi_fn;
use cargo_near_integration_tests::{generate_abi_fn, generate_abi_with};
use function_name::named;
use schemars::r#gen::SchemaGenerator;

Expand Down Expand Up @@ -46,3 +46,42 @@ fn test_simple_function() -> cargo_near::CliResult {

Ok(())
}

#[test]
#[named]
fn test_abi_with_unguarded_no_mangle_function() -> cargo_near::CliResult {
let abi_root = generate_abi_with! {
Code:
use near_sdk::near;

#[near(contract_state)]
#[derive(Default)]
pub struct Contract {}

#[near]
impl Contract {
pub fn get_value(&self) -> u32 {
42
}
}

#[unsafe(no_mangle)]
pub extern "C" fn custom_function() {
unsafe {
near_sdk::sys::input(0);
}
}
};

let function_names = abi_root
.body
.functions
.iter()
.map(|function| function.name.as_str())
.collect::<Vec<_>>();

assert!(function_names.contains(&"get_value"));
assert!(!function_names.contains(&"custom_function"));

Ok(())
}
49 changes: 48 additions & 1 deletion integration-tests/tests/build/opts.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::util;
use cargo_near_integration_tests::{build_fn_with, setup_tracing};
use cargo_near_integration_tests::{build_fn_with, build_with, setup_tracing};
use function_name::named;
use std::fs;

Expand Down Expand Up @@ -123,6 +123,53 @@ async fn test_build_custom_profile() -> testresult::TestResult {
Ok(())
}

#[tokio::test]
#[named]
async fn test_build_with_unguarded_no_mangle_function() -> testresult::TestResult {
setup_tracing();
let build_result = build_with! {
Code:
use near_sdk::near;

#[near(contract_state)]
#[derive(Default)]
pub struct Contract {}

#[near]
impl Contract {
pub fn get_value(&self) -> u32 {
42
}
}

#[unsafe(no_mangle)]
pub extern "C" fn custom_function() {
unsafe {
near_sdk::sys::input(0);
}
}
};

let abi_root = build_result
.abi_root
.expect("ABI should be generated for the contract");
let function_names = abi_root
.body
.functions
.iter()
.map(|function| function.name.as_str())
.collect::<Vec<_>>();
assert!(function_names.contains(&"get_value"));
assert!(!function_names.contains(&"custom_function"));

let worker = near_workspaces::sandbox().await?;
let contract = worker.dev_deploy(&build_result.wasm).await?;
let outcome = contract.call("get_value").view().await?;
assert_eq!(outcome.json::<u32>()?, 42);

Ok(())
}

#[tokio::test]
#[named]
async fn test_build_abi_features_separate_from_wasm_features() -> testresult::TestResult {
Expand Down
Loading