Skip to content
Merged
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
14 changes: 13 additions & 1 deletion src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ else
CARGO_FEATURE_TRACY =
endif

# Enable the custom tcmalloc global allocator in the Rust crate whenever the C++
# build links tcmalloc (see configure.ac). The global allocator lives only in
# the top-level stellar-core crate, so this is intentionally not passed to the
# soroban sub-crate builds below.
if USE_TCMALLOC
CARGO_FEATURE_TCMALLOC = --features tcmalloc
else
CARGO_FEATURE_TCMALLOC =
endif

# Generated sources that are built separately (not in SRC_CXX_FILES)
# These are reused by both stellar-core and fuzz targets.
GENERATED_VERSION_SOURCES = main/StellarCoreVersion.cpp main/XDRFilesSha256.cpp
Expand Down Expand Up @@ -345,7 +355,8 @@ $(LIBRUST_STELLAR_CORE): $(RUST_HOST_DEPFILES) $(SRC_RUST_FILES) Makefile $(RUST
$(RUST_PROFILE_ARG) \
--locked \
--target-dir $(abspath $(RUST_TARGET_DIR)) \
$(CARGO_FEATURE_FASTDEV) $(CARGO_FEATURE_TRACY) $(CARGO_FEATURE_NEXT) $(CARGO_FEATURE_TESTUTILS)
$(CARGO_FEATURE_FASTDEV) $(CARGO_FEATURE_TRACY) $(CARGO_FEATURE_NEXT) $(CARGO_FEATURE_TESTUTILS) \
$(CARGO_FEATURE_TCMALLOC)

else # !ENABLE_FASTDEV_UNSAFE_FOR_PRODUCTION

Expand Down Expand Up @@ -456,6 +467,7 @@ $(LIBRUST_STELLAR_CORE): $(RUST_HOST_DEPFILES) $(SRC_RUST_FILES) $(ALL_SOROBAN_L
--locked \
--target-dir $(abspath $(RUST_TARGET_DIR)) \
$(CARGO_FEATURE_TRACY) $(CARGO_FEATURE_NEXT) $(CARGO_FEATURE_TESTUTILS) \
$(CARGO_FEATURE_TCMALLOC) \
-- \
$(ALL_SOROBAN_EXTERN_ARGS) \
$(ALL_SOROBAN_DEPEND_ARGS)
Expand Down
6 changes: 6 additions & 0 deletions src/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ fastdev = ["dep:soroban-env-host-p27",

tracy = ["dep:tracy-client"]

# Use the C++ tcmalloc (gperftools) allocator that is linked into stellar-core
# as the Rust global allocator (see src/tcmalloc.rs). This is enabled by the
# build system (src/Makefile.am) whenever the C++ build uses tcmalloc
# (USE_TCMALLOC). Builds without it use Rust's default allocator.
tcmalloc = []

# The "next" feature enables not-yet-released features that (a) break protocol
# but (b) are still under development, in between protocol releases. This
# feature is not enabled by default, and is only intended for temporary use when
Expand Down
11 changes: 11 additions & 0 deletions src/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ mod soroban_test_extra_protocol;

mod soroban_fuzz;

// When built against a C++ tcmalloc, route all Rust allocations (including the
// linked-in soroban hosts) through it via a custom global allocator. See
// tcmalloc.rs for details. Gated on the `tcmalloc` feature so tcmalloc-less
// builds keep using Rust's default allocator.
#[cfg(feature = "tcmalloc")]
mod tcmalloc;

#[cfg(feature = "tcmalloc")]
#[global_allocator]
static GLOBAL_ALLOCATOR: tcmalloc::TcMalloc = tcmalloc::TcMalloc;

use soroban_module_cache::SorobanModuleCache;

mod bridge;
Expand Down
83 changes: 83 additions & 0 deletions src/rust/src/tcmalloc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2026 Stellar Development Foundation and contributors. Licensed
// under the Apache License, Version 2.0. See the COPYING file at the root
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

//! Rust global allocator backed by the C++ tcmalloc (gperftools) allocator that
//! is linked into stellar-core. This is only compiled with the `tcmalloc`
//! feature, which the build system enables whenever the C++ build uses tcmalloc
//! (see `USE_TCMALLOC` in configure.ac / src/Makefile.am). Builds without
//! tcmalloc (sanitizer or non-Linux builds) simply use Rust's default
//! allocator.
//!
//! The `tc_*` symbols are provided by `libtcmalloc_minimal.a` and are left
//! undefined in `librust_stellar_core.a`; they are resolved at the final C++
//! link step, exactly like the `shim_*` C++ functions imported by the bridge.
//! No `#[link]` attribute or build script is required.

use core::ffi::c_void;
use std::alloc::{GlobalAlloc, Layout};

extern "C" {
fn tc_malloc(size: usize) -> *mut c_void;
fn tc_free(ptr: *mut c_void);
fn tc_realloc(ptr: *mut c_void, size: usize) -> *mut c_void;
fn tc_calloc(nmemb: usize, size: usize) -> *mut c_void;
fn tc_memalign(alignment: usize, size: usize) -> *mut c_void;
}

// tcmalloc, like the system malloc, guarantees alignment suitable for any
// fundamental type (alignof(max_align_t)): 16 bytes on 64-bit, 8 on 32-bit.
// Requests with a larger alignment need to go through tc_memalign.
#[cfg(target_pointer_width = "64")]
const MIN_ALIGN: usize = 16;
#[cfg(target_pointer_width = "32")]
const MIN_ALIGN: usize = 8;

pub struct TcMalloc;

// This implementation mirrors the standard library's unix allocator (
// https://github.com/rust-lang/rust/blob/225e91c03da22cd4b9792b83c1cfc97967101614/library/std/src/sys/alloc/unix.rs).
// Normally aligned allocations use tc_malloc/tc_calloc/tc_realloc, over-aligned
// ones use tc_memalign, and tc_free deallocates memory from any of these.
unsafe impl GlobalAlloc for TcMalloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if layout.align() <= MIN_ALIGN && layout.align() <= layout.size() {
tc_malloc(layout.size()) as *mut u8
} else {
tc_memalign(layout.align(), layout.size()) as *mut u8
}
}

unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
if layout.align() <= MIN_ALIGN && layout.align() <= layout.size() {
tc_calloc(1, layout.size()) as *mut u8
} else {
let ptr = self.alloc(layout);
if !ptr.is_null() {
core::ptr::write_bytes(ptr, 0, layout.size());
}
ptr
}
}

unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
tc_free(ptr as *mut c_void);
}

unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
if layout.align() <= MIN_ALIGN && layout.align() <= new_size {
tc_realloc(ptr as *mut c_void, new_size) as *mut u8
} else {
// tc_realloc cannot preserve an over-aligned allocation's alignment,
// so allocate fresh, copy, and free the old block by hand.
let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
let new_ptr = self.alloc(new_layout);
if !new_ptr.is_null() {
let copy = core::cmp::min(layout.size(), new_size);
core::ptr::copy_nonoverlapping(ptr, new_ptr, copy);
self.dealloc(ptr, layout);
}
new_ptr
}
}
}
Loading