Выделение памяти — это не самая простая задача, и 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); }#![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() {}#![feature(alloc_jemalloc)] #![crate_type = "dylib"] extern crate alloc_jemalloc; pub fn foo() { let a = Box::new(4); // выделение памяти с помощью jemalloc println!("{}", a); }
Иногда даже выбора между jemalloc и системным менеджером недостаточно и
необходим совершенно новый менеджер памяти. В этом случае мы напишем наш
собственный контейнер, который будет предоставлять API менеджера памяти (также
как и alloc_system
или alloc_jemalloc
). Для примера давайте рассмотрим
упрощенную и аннотированную версию alloc_system
:
// Компилятору нужно указать, что этот контейнер является менеджером памяти, для // того что бы при компоновке он не использовал другой менеджер. #![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 }
После того как мы скомпилировали этот контейнер, мы можем использовать его следующим образом:
extern crate my_allocator; fn main() { let a = Box::new(8); // выделение памяти с помощью нашего контейнера println!("{}", a); }extern crate my_allocator; fn main() { let a = Box::new(8); // выделение памяти с помощью нашего контейнера println!("{}", a); }
Несколько ограничений при работе с пользовательским менеджером памяти, которые могут быть причиной ошибок компиляции:
Любой артефакт может быть скомпонован только с одним менеджером. Исполняемые файлы, динамические библиотеки и статические библиотеки должны быть скомпонованы с одним менеджером, и если не один не был указан, то компилятор сам выберет один. В то же время Rust библиотеки (rlibs) не нуждаются в компоновке с менеджером (но это возможно).
Потребитель какого-либо менеджера памяти имеет пометку #![needs_allocator]
(в данном случае контейнер liballoc
) и какой-либо контейнер #[allocator]
не может транзитивно зависеть от контейнера, которому нужен менеджер (т.е.
циклическая зависимость не допускается). Это означает, что менеджеры памяти в
данный момент должны ограничить себя только libcore.