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()?;

Comment on lines +45 to +51
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load_near_host_function_stubs() is executed on every ABI extraction on Unix, which means spawning rustc and compiling a cdylib even when the target dylib has no unresolved host symbols. This adds noticeable overhead to cargo near abi/cargo near build (and to integration tests) and may also complicate environments with restricted process spawning. Consider compiling/loading the stubs lazily only when dlopen fails due to an undefined symbol, or caching the compiled+loaded stubs with a OnceLock so it happens at most once per process.

Copilot uses AI. Check for mistakes.
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;
Comment on lines +82 to +84
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On macOS this unconditionally pushes a new RUSTFLAGS value into compile_env, which will override any RUSTFLAGS already present in the env slice (because cargo_native::compile::run collects env into a map). This is inconsistent with the WASM build path (where user-provided env vars are allowed to override tool defaults) and can break projects that rely on custom RUSTFLAGS during ABI generation. Consider reading any existing RUSTFLAGS from env and appending -C link-arg=-undefined -C link-arg=dynamic_lookup (and -Awarnings when needed) rather than replacing the value entirely.

Suggested change
if let Some(rustflags) = abi_generation_rustflags(hide_warnings) {
compile_env.push((env_keys::RUSTFLAGS, rustflags));
hide_warnings_for_compile = false;
let has_rustflags_override = env.iter().any(|(key, _)| *key == env_keys::RUSTFLAGS);
if !has_rustflags_override {
if let Some(rustflags) = abi_generation_rustflags(hide_warnings) {
compile_env.push((env_keys::RUSTFLAGS, rustflags));
hide_warnings_for_compile = false;
}

Copilot uses AI. Check for mistakes.
}

(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