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

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

// 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 () {}
// Компилятору нужно указать, что этот контейнер является менеджером памяти, для
// того что бы при компоновке он не использовал другой менеджер.
#![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);
}

Ограничения пользовательских менеджеров памяти

Несколько ограничений при работе с пользовательским менеджером памяти, которые могут быть причиной ошибок компиляции: