% Пользовательские менеджеры памяти
Выделение памяти — это не самая простая задача, и Rust обычно заботится об этом сам, но часто нужно тонко управлять выделением памяти. Компилятор и стандартная библиотека в настоящее время позволяют глобально переключить используемый менеджер во время компиляции. Описание сейчас находится в RFC 1183, но здесь мы рассмотрим как сделать ваш собственный менеджер.
Стандартный менеджер памяти
В настоящее время компилятор содержит два стандартных менеджера: alloc_system
и alloc_jemalloc
(однако у некоторых платформ отсутствует jemalloc).
Эти менеджеры стандартны для контейнеров Rust и содержат реализацию подпрограмм
для выделения и освобождения памяти. Стандартная библиотека не компилируется
специально для использования только одного из них. Компилятор будет решать какой
менеджер использовать во время компиляции в зависимости от типа производимых
выходных артефактов.
По умолчанию исполняемые файлы сгенерированные компилятором будут использовать
alloc_jemalloc
(там где возможно). В таком случае компилятор "контролирует
весь мир", в том смысле что у него есть власть над окончательной компоновкой.
Однако динамические и статические библиотеки по умолчанию будут использовать
alloc_system
. Здесь Rust обычно в роли гостя в другом приложении или вообще в
другом мире, где он не может авторитетно решать какой менеджер использовать.
В результате он возвращается назад к стандартным API (таких как malloc
и
free
), для получения и освобождения памяти.
Переключение менеджеров памяти
Несмотря на то что в большинстве случаев нам подойдёт то, что компилятор выбирает по умолчанию, часто бывает необходимо настроить определенные аспекты. Для того, чтобы переопределить решение компилятора о том, какой именно менеджер использовать, достаточно просто скомпоновать с желаемым менеджером:
#![feature(alloc_system)]
extern crate alloc_system;
fn main() {
let a = Box::new(4); // выделение памяти с помощью системного менеджера
println!("{}", a);
}
В этом примере сгенерированный исполняемый файл будет скомпонован с системным менеджером, вместо менеджера по умолчанию — jemalloc. И наоборот, чтобы сгенерировать динамическую библиотеку, которая использует jemalloc по умолчанию нужно написать:
#![feature(alloc_jemalloc)]
#![crate_type = "dylib"]
extern crate alloc_jemalloc;
pub fn foo() {
let a = Box::new(4); // выделение памяти с помощью jemalloc
println!("{}", a);
}
# fn main() {}
Написание своего менеджера памяти
Иногда даже выбора между jemalloc и системным менеджером недостаточно и
необходим совершенно новый менеджер памяти. В этом случае мы напишем наш
собственный контейнер, который будет предоставлять API менеджера памяти (также
как и alloc_system
или alloc_jemalloc
). Для примера давайте рассмотрим
упрощенную и аннотированную версию alloc_system
:
# // only needed for rustdoc --test down below
# #![feature(lang_items)]
// Компилятору нужно указать, что этот контейнер является менеджером памяти, для
// того что бы при компоновке он не использовал другой менеджер.
#![feature(allocator)]
#![allocator]
// Менеджерам памяти не позволяют зависеть от стандартной библиотеки, которая в
// свою очередь зависит от менеджера, чтобы избежать циклической зависимости.
// Однако этот контейнер может использовать все из libcore.
#![no_std]
// Давайте дадим какое-нибудь уникальное имя нашему менеджеру.
#![crate_name = "my_allocator"]
#![crate_type = "rlib"]
// Наш системный менеджер будет использовать поставляемый вместе с компилятором
// контейнер libc для связи с FFI. Имейте ввиду, что на данный момент внешний
// (crates.io) libc не может быть использован, поскольку он компонуется со
// стандартной библиотекой (`#![no_std]` все еще нестабилен).
#![feature(libc)]
extern crate libc;
// Ниже перечислены пять функций, необходимые пользовательскому менеджеру памяти.
// Их сигнатуры и имена на данный момент не проверяются компилятором, но это
// вскоре будет реализовано, так что они должны соответствовать тому, что
// находится ниже.
//
// Имейте ввиду, что стандартные `malloc` и `realloc` не предоставляют опций для
// выравнивания, так что эта реализация должна быть улучшена и поддерживать
// выравнивание.
#[no_mangle]
pub extern fn __rust_allocate(size: usize, _align: usize) -> *mut u8 {
unsafe { libc::malloc(size as libc::size_t) as *mut u8 }
}
#[no_mangle]
pub extern fn __rust_deallocate(ptr: *mut u8, _old_size: usize, _align: usize) {
unsafe { libc::free(ptr as *mut libc::c_void) }
}
#[no_mangle]
pub extern fn __rust_reallocate(ptr: *mut u8, _old_size: usize, size: usize,
_align: usize) -> *mut u8 {
unsafe {
libc::realloc(ptr as *mut libc::c_void, size as libc::size_t) as *mut u8
}
}
#[no_mangle]
pub extern fn __rust_reallocate_inplace(_ptr: *mut u8, old_size: usize,
_size: usize, _align: usize) -> usize {
old_size // libc не поддерживает этот API
}
#[no_mangle]
pub extern fn __rust_usable_size(size: usize, _align: usize) -> usize {
size
}
# // just needed to get rustdoc to test this
# fn main() {}
# #[lang = "panic_fmt"] fn panic_fmt() {}
# #[lang = "eh_personality"] fn eh_personality() {}
# #[lang = "eh_unwind_resume"] extern fn eh_unwind_resume() {}
# #[no_mangle] pub extern fn rust_eh_register_frames () {}
# #[no_mangle] pub extern fn rust_eh_unregister_frames () {}
После того как мы скомпилировали этот контейнер, мы можем использовать его следующим образом:
extern crate my_allocator;
fn main() {
let a = Box::new(8); // выделение памяти с помощью нашего контейнера
println!("{}", a);
}
Ограничения пользовательских менеджеров памяти
Несколько ограничений при работе с пользовательским менеджером памяти, которые могут быть причиной ошибок компиляции:
Любой артефакт может быть скомпонован только с одним менеджером. Исполняемые файлы, динамические библиотеки и статические библиотеки должны быть скомпонованы с одним менеджером, и если не один не был указан, то компилятор сам выберет один. В то же время Rust библиотеки (rlibs) не нуждаются в компоновке с менеджером (но это возможно).
Потребитель какого-либо менеджера памяти имеет пометку
#![needs_allocator]
(в данном случае контейнерliballoc
) и какой-либо контейнер#[allocator]
не может транзитивно зависеть от контейнера, которому нужен менеджер (т.е. циклическая зависимость не допускается). Это означает, что менеджеры памяти в данный момент должны ограничить себя только libcore.