Пользовательские менеджеры памяти

Выделение памяти — это не самая простая задача, и 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.