Компоновка (Linkage)
Note
Этот раздел описывается скорее с точки зрения компилятора, чем языка.
Компилятор поддерживает различные методы для компоновки крейтов вместе как статически, так и динамически. В этом разделе будут рассмотрены различные методы компоновки крейтов, а дополнительная информация о нативных библиотеках может быть найдена в разделе FFI книги.
rffi: ../book/ch19-01-unsafe-rust.html#using-extern-functions-to-call-external-code
За одну сессию компиляции компилятор может сгенерировать несколько артефактов с использованием либо флагов командной строки, либо атрибута crate_type. Если указан один или несколько флагов командной строки, все атрибуты crate_type будут проигнорированы в пользу сборки только тех артефактов, которые указаны в командной строке.
--crate-type=bin,#![crate_type = "bin"]- Будет создан запускаемый исполняемый файл. Это требует наличия функцииmainв крейте, которая будет запущена при начале выполнения программы. Это слинкует все зависимости Rust и нативные зависимости, производя единый распространяемый бинарный файл. Это тип крейта по умолчанию.
--crate-type=lib,#![crate_type = "lib"]- Будет создана библиотека Rust. Это неоднозначное понятие, потому что библиотека может проявляться в нескольких формах. Цель этой общей опцииlib- сгенерировать “рекомендуемый компилятором” стиль библиотеки. Выходная библиотека всегда будет пригодна для использования rustc, но фактический тип библиотеки может меняться время от времени. Оставшиеся типы вывода - это все различные варианты библиотек, и типlibможно рассматривать как псевдоним одного из них (но фактический выбор определяется компилятором).
--crate-type=dylib,#![crate_type = "dylib"]- Будет создана динамическая библиотека Rust. Это отличается от типа выводаlibтем, что это принудительно генерирует динамическую библиотеку. Полученная динамическая библиотека может быть использована как зависимость для других библиотек и/или исполняемых файлов. Этот тип вывода создаст файлы*.soна Linux,*.dylibна macOS и*.dllна Windows.
-
--crate-type=staticlib,#![crate_type = "staticlib"]- Будет создана статическая системная библиотека. Это отличается от других выходных форматов библиотек тем, что компилятор никогда не будет пытаться линковаться с выводамиstaticlib. Цель этого типа вывода - создать статическую библиотеку, содержащую весь код локального крейта вместе со всеми восходящими зависимостями. Этот тип вывода создаст файлы*.aна Linux, macOS и Windows (MinGW) и файлы*.libна Windows (MSVC). Этот формат рекомендуется для использования в таких ситуациях, как связывание кода Rust в существующее приложение не на Rust, потому что у него не будет динамических зависимостей от другого кода Rust.Обратите внимание, что любые динамические зависимости, которые может иметь статическая библиотека (такие как зависимости от системных библиотек или зависимости от библиотек Rust, которые скомпилированы как динамические библиотеки), должны быть указаны вручную при линковке этой статической библиотеки откуда-либо. Флаг
--print=native-static-libsможет помочь с этим.Обратите внимание, что, поскольку результирующая статическая библиотека содержит код всех зависимостей, включая стандартную библиотеку, а также экспортирует все их публичные символы, линковка статической библиотеки в исполняемый файл или общую библиотеку может потребовать особого внимания. В случае общей библиотеки список экспортируемых символов должен быть ограничен с помощью, например, скрипта линковщика или скрипта управления версиями символов, списка экспортируемых символов (macOS) или файла определений модуля (Windows). Дополнительно, неиспользуемые секции могут быть удалены, чтобы убрать весь код зависимостей, который фактически не используется (например,
--gc-sectionsили-dead_stripдля macOS).
--crate-type=cdylib,#![crate_type = "cdylib"]- Будет создана динамическая системная библиотека. Это используется при компиляции динамической библиотеки для загрузки из другого языка. Этот тип вывода создаст файлы*.soна Linux,*.dylibна macOS и*.dllфайлы на Windows.
--crate-type=rlib,#![crate_type = "rlib"]- Будет создан файл “библиотеки Rust”. Это используется как промежуточный артефакт, и его можно рассматривать как “статическую библиотеку Rust”. Эти файлыrlib, в отличие от файловstaticlib, интерпретируются компилятором при будущей линковке. Это essentially означает, чтоrustcбудет искать метаданные в файлахrlibтак же, как он ищет метаданные в динамических библиотеках. Эта форма вывода используется для производства статически слинкованных исполняемых файлов, а также выводовstaticlib.
--crate-type=proc-macro,#![crate_type = "proc-macro"]- Производимый вывод не специфицирован, но если к нему предоставлен путь-L, то компилятор распознает выходные артефакты как макрос, и он может быть загружен для программы. Крейты, скомпилированные с этим типом крейта, должны экспортировать только процедурные макросы. Компилятор автоматически установит опцию конфигурацииproc_macro. Крейты всегда компилируются с той же целью, с которой был построен сам компилятор. Например, если вы запускаете компилятор из Linux с процессоромx86_64, целью будетx86_64-unknown-linux-gnu, даже если крейт является зависимостью другого крейта, который строится для другой цели.
Обратите внимание, что эти выходные данные являются складываемыми в том смысле, что если указано несколько, то компилятор произведет каждую форму вывода без необходимости перекомпиляции. Однако это применяется только для выходных данных, указанных одним и тем же методом. Если указаны только атрибуты crate_type, то все они будут построены, но если указаны один или несколько флагов командной строки --crate-type, то будут построены только эти выходные данные.
Со всеми этими различными видами выходных данных, если крейт A зависит от крейта B, то компилятор может найти B в различных формах по всей системе. Однако единственные формы, которые ищет компилятор, - это формат rlib и формат динамической библиотеки. Имея эти два варианта для зависимой библиотеки, компилятор в какой-то момент должен сделать выбор между этими двумя форматами. Имея это в виду, компилятор следует этим правилам при определении того, какой формат зависимостей будет использоваться:
-
Если производится статическая библиотека, все восходящие зависимости должны быть доступны в форматах
rlib. Это требование вытекает из того, что динамическую библиотеку нельзя преобразовать в статический формат.Обратите внимание, что невозможно линковать нативные динамические зависимости в статическую библиотеку, и в этом случае будут выводиться предупреждения о всех неслинкованных нативных динамических зависимостях.
-
Если производится файл
rlib, то нет ограничений на то, в каком формате доступны восходящие зависимости. Просто требуется, чтобы все восходящие зависимости были доступны для чтения метаданных из них.Причина этого в том, что файлы
rlibне содержат никаких своих восходящих зависимостей. Было бы не очень эффективно, если бы все файлыrlibсодержали копиюlibstd.rlib!
- Если производится исполняемый файл и флаг
-C prefer-dynamicне указан, то сначала предпринимается попытка найти зависимости в форматеrlib. Если некоторые зависимости недоступны в формате rlib, то предпринимается попытка динамической линковки (см. ниже).
-
Если производится динамическая библиотека или исполняемый файл, который линкуется динамически, то компилятор попытается согласовать доступные зависимости в формате rlib или dylib, чтобы создать конечный продукт.
Основная цель компилятора - обеспечить, чтобы библиотека никогда не появлялась более одного раза в любом артефакте. Например, если динамические библиотеки B и C были статически слинкованы с библиотекой A, то крейт не может слинковаться с B и C вместе, потому что было бы две копии A. Компилятор позволяет смешивать форматы rlib и dylib, но это ограничение должно быть соблюдено.
Компилятор в настоящее время не реализует никакого метода указания, с каким форматом должна быть слинкована библиотека. При динамической линковке компилятор попытается максимизировать динамические зависимости, все еще позволяя некоторым зависимостям быть слинкованными через rlib.
Для большинства ситуаций рекомендуется иметь все библиотеки доступными как dylib, если выполняется динамическая линковка. Для других ситуаций компилятор выдаст предупреждение, если он не сможет определить, с какими форматами линковать каждую библиотеку.
В общем, --crate-type=bin или --crate-type=lib должно быть достаточно для всех потребностей компиляции, а другие опции просто доступны, если требуется более детальный контроль над форматом вывода крейта.
Статические и динамические C рантаймы
Стандартная библиотека в целом стремится поддерживать как статически линкованные, так и динамически линкованные C рантаймы для целей, где это уместно. Например, цели x86_64-pc-windows-msvc и x86_64-unknown-linux-musl обычно поставляются с обоими рантаймами, и пользователь выбирает, какой он хотел бы. Все цели в компиляторе имеют режим по умолчанию для линковки с C рантаймом. Обычно цели линкуются динамически по умолчанию, но есть исключения, которые являются статическими по умолчанию, такие как:
arm-unknown-linux-musleabiarm-unknown-linux-musleabihfarmv7-unknown-linux-musleabihfi686-unknown-linux-muslx86_64-unknown-linux-musl
Линковка C рантайма настроена на соблюдение целевой функции crt-static. Эти целевые функции обычно настраиваются из командной строки через флаги для самого компилятора. Например, чтобы включить статический рантайм, вы должны выполнить:
rustc -C target-feature=+crt-static foo.rs
тогда как для динамической линковки с C рантаймом вы должны выполнить:
rustc -C target-feature=-crt-static foo.rs
Цели, которые не поддерживают переключение между линковкой C рантайма, проигнорируют этот флаг. Рекомендуется проверить полученный бинарный файл, чтобы убедиться, что он слинкован так, как вы ожидаете, после успешного завершения работы компилятора.
Крейты также могут узнавать о том, как линкуется C рантайм. Код на MSVC, например, должен компилироваться по-разному (например, с /MT или /MD) в зависимости от линкуемого рантайма. В настоящее время это экспортируется через опцию target_feature атрибута cfg:
#![allow(unused)] fn main() { #[cfg(target_feature = "crt-static")] fn foo() { println!("C рантайм должен быть статически слинкован"); } #[cfg(not(target_feature = "crt-static"))] fn foo() { println!("C рантайм должен быть динамически слинкован"); } }
Также обратите внимание, что скрипты сборки Cargo могут узнавать об этой функции через переменные окружения. В скрипте сборки вы можете обнаружить линковку через:
use std::env; fn main() { let linkage = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or(String::new()); if linkage.contains("crt-static") { println!("C рантайм будет статически слинкован"); } else { println!("C рантайм будет динамически слинкован"); } }
Чтобы использовать эту функцию локально, вы обычно будете использовать переменную окружения RUSTFLAGS для указания флагов компилятору через Cargo. Например, чтобы скомпилировать статически слинкованный бинарный файл на MSVC, вы должны выполнить:
RUSTFLAGS='-C target-feature=+crt-static' cargo build --target x86_64-pc-windows-msvc
Смешанные кодовые базы Rust и кода других языков
Если вы смешиваете Rust с кодом других языков (например, C, C++) и хотите создать единый бинарный файл, содержащий оба типа кода, у вас есть два подхода для финальной линковки бинарного файла:
- Используйте
rustc. Передавайте любые не-Rust библиотеки, используя аргументы-L <directory>и-l<library>в rustc, и/или директивы#[link]в вашем коде Rust. Если вам нужно линковаться с файлами.o, вы можете использовать-Clink-arg=file.o. - Используйте ваш линковщик другого языка. В этом случае вам сначала нужно сгенерировать цель
staticlibRust и передать ее в вызов вашего линковщика. Если вам нужно слинковать несколько подсистем Rust, вам нужно будет сгенерировать единственныйstaticlib, возможно, используя множество операторовextern crateдля включения несколькихrlibRust. Несколько файловstaticlibRust, скорее всего, будут конфликтовать.
Прямая передача rlib в ваш линковщик другого языка в настоящее время не поддерживается.
Note
Код Rust, скомпилированный или слинкованный с другим экземпляром рантайма Rust, считается “внешним кодом” для целей этого раздела.
Запрещенная линковка и раскрутка стека
Раскрутка стека при панике (unwinding) может быть использована только если бинарный файл построен последовательно в соответствии со следующими правилами.
Артефакт Rust называется потенциально раскручивающим, если выполняется любое из следующих условий:
- Артефакт использует обработчик паники
unwind. - Артефакт содержит крейт, собранный со стратегией паники
unwindpanic strategy, который совершает вызов функции, используя ABI-unwind. - Артефакт совершает вызов ABI
"Rust"в код, выполняющийся в другом артефакте Rust, который имеет отдельную копию рантайма Rust, и этот другой артефакт является потенциально раскручивающим.
Note
Это определение отражает, может ли вызов ABI
"Rust"внутри артефакта Rust когда-либо раскручивать стек.
Если артефакт Rust является потенциально раскручивающим, то все его крейты должны быть собраны со стратегией паники unwind panic strategy. В противном случае раскрутка стека может вызвать неопределенное поведение.
Note
Если вы используете
rustcдля линковки, эти правила применяются автоматически. Если вы не используетеrustcдля линковки, вы должны позаботиться о том, чтобы раскрутка стека обрабатывалась последовательно во всем бинарном файле. Линковка безrustcвключает использованиеdlopenили подобных средств, где линковка выполняется системным рантаймом без участияrustc. Это может произойти только при смешивании кода с разными флагами-C panic, поэтому большинству пользователей не нужно беспокоиться об этом.
Note
Чтобы гарантировать, что библиотека будет корректной (и линкуемой с
rustc) независимо от используемого рантайма паники во время линковки, может использоваться lintffi_unwind_calls. Этот lint помечает любые вызовы-unwindиностранных функций или указателей на функции.