Перейти к основному содержимому

5.06. Библиотеки

Разработчику Архитектору

Библиотеки в C++

Современные программные системы редко создаются «с нуля» в буквальном смысле. Даже минимальное приложение на C++ почти всегда опирается на внешние компоненты — от базовых операций ввода-вывода до сложных алгоритмов обработки данных. Ключевым механизмом, обеспечивающим повторное использование кода, его инкапсуляцию и независимую поставку, являются библиотеки.

В широком смысле библиотека в C++ — это совокупность объявлений и реализаций, организованных таким образом, чтобы их можно было использовать в произвольных проектах без необходимости повторного написания или копирования исходного кода. Это не просто коллекция функций: библиотека представляет собой продуманную архитектурную единицу, с чёткими интерфейсами, контрактами поведения и стратегиями развёртывания. Она позволяет разделять ответственность между разработчиками, ускорять сборку, уменьшать объём итогового исполняемого файла и повышать стабильность программного продукта.

Существует два базовых типа библиотек: статические и динамические. Статическая библиотека встраивается в исполняемый файл на этапе компоновки, становясь его неотъемлемой частью. Динамическая библиотека остаётся отдельным файлом (например, .dll в Windows или .so в Linux) и загружается в память во время выполнения. Однако независимо от способа связывания, для использования библиотеки разработчик сначала должен получить доступ к её интерфейсу — именно здесь в игру вступает препроцессор и механизм подключения заголовочных файлов.


Препроцессор

Прежде чем компилятор начинает преобразовывать C++-код в машинные инструкции, исходный текст проходит этап предварительной обработки, или препроцессинга. Эту работу выполняет препроцессор — отдельная утилита, входящая в состав инструментария компилятора (например, cpp в GCC или clang -E в Clang).

Препроцессор не знает ничего о синтаксисе C++ как таковом. Он работает на уровне текста, обрабатывая специальные команды, называемые директивами препроцессора. Все директивы начинаются с символа # и должны располагаться в начале строки. Наиболее известная из них — #include — лежит в основе всей системы библиотек в C++.

Суть действия #include <имя> или #include "имя" проста по концепции, но глубока по последствиям: препроцессор фактически вставляет всё содержимое указанного файла на место директивы. Это не ссылка, не импорт в стиле Python — это буквальная текстовая подстановка. Если включить <iostream>, то в месте директивы окажется весь исходный текст заголовочного файла этой библиотеки — десятки (иногда сотни) строк объявлений шаблонов, определений классов, inline-функций и специализаций.

Такая модель обеспечивает полную прозрачность интерфейса: компилятор видит ровно тот код, который ему необходим для проверки типов, разрешения имён и генерации корректного машинного кода. В то же время она накладывает определённые требования на структуру заголовочных файлов — в частности, необходимость защиты от повторного включения (через #ifndef/#define/#endif или #pragma once), иначе одна и та же сущность может быть объявлена дважды, что приведёт к ошибке компиляции.

Важно подчеркнуть: препроцессор работает до компиляции, и его действия необратимы. Результат его работы — так называемый preprocessed translation unit — это единый, «сплющенный» файл, в котором уже нет директив #include, #define, условной компиляции и прочего препроцессорного синтаксиса. Именно этот текст поступает на вход компилятору. Поэтому, хотя препроцессор незаметен в повседневной разработке, его роль фундаментальна: без него механизм библиотек в классическом C/C++ просто не существовал бы.


Заголовочные файлы

Каждая полноценная библиотека на C++ предполагает разделение на интерфейс и реализацию. Интерфейс описывает, что может делать библиотека, но не как. Он фиксируется в заголовочных файлах — обычно с расширением .h или .hpp.

Заголовочный файл содержит:

  • объявления функций (без тел),
  • объявления классов (без определений методов, за исключением inline),
  • определения шаблонов (поскольку шаблоны должны быть видны в точке инстанцирования),
  • определения констант, типов (using, typedef, enum class),
  • директивы условной компиляции, управляющие доступностью интерфейсов.

Например, в <cmath> мы не найдём реализации std::sqrt — там будет только её объявление:

double sqrt(double x);
float sqrt(float x);
long double sqrt(long double x);

Реализация же компилируется заранее и поставляется либо в виде объектных файлов (в составе статической библиотеки .a/.lib), либо в составе динамической библиотеки времени выполнения (например, libm.so в Linux или msvcrt.dll в Windows).

При этом существуют два способа подключения заголовков:

  1. #include <имя> — используется для системных и стандартных заголовков. Поиск происходит в путях, заданных компилятором по умолчанию (например, /usr/include/c++/11/ в GCC). Препроцессор не ищет такие файлы в текущей директории проекта.

  2. #include "имя" — используется для пользовательских заголовков. Сначала поиск ведётся в том же каталоге, где находится текущий исходный файл (или в путях, переданных через -I). Если файл не найден, препроцессор повторяет поиск как для <>. Это поведение важно: оно позволяет локально переопределять системные заголовки — при крайней необходимости и с осторожностью.

C++ постепенно движется в сторону уменьшения зависимости от препроцессора. Например, модули (C++20), призванные заменить #include, работают на уровне семантических интерфейсов. Однако на момент 2025 года поддержка модулей остаётся неполной в большинстве промышленных окружений, поэтому понимание препроцессорной модели остаётся критически важным.


Стандартная библиотека C++

Стандартная библиотека C++ (часто обозначаемая как stdlib или libstdc++/libc++, в зависимости от реализации) — это иерархически организованная система компонентов, развивавшаяся десятилетиями. Её корни уходят в стандартную библиотеку C, но C++ расширил её фундаментально — внедрением принципиально новых абстракций: обобщённого программирования, управления ресурсами посредством RAII, поддержки многопоточности на уровне языка.

Важно понимать: стандартная библиотека — это часть стандарта языка. Это означает, что любой соответствующий стандарту компилятор обязан предоставлять её в полном объёме (с оговорками на экспериментальные или отложенные фичи). Она не является «опциональной надстройкой» — она составляет ту среду, в которой C++-программа существует. Без неё невозможно написать даже простейшее консольное приложение.

Ядро

Самый заметный и влиятельный компонент — Стандартная библиотека шаблонов (STL), хотя в современном стандарте термин «STL» формально не используется: её идеи полностью интегрированы в общую структуру std. Основу STL составляют три взаимосвязанных понятия, образующих единую философию:

  • Контейнеры — это структуры данных, инкапсулирующие хранение элементов. Они предоставляют единообразный интерфейс для добавления, удаления, доступа и перебора. Важно, что контейнеры не знают ничего о типе хранимых данных: они реализованы как шаблоны классов. Например, std::vector<T> может хранить целые числа, строки, пользовательские объекты — при условии, что тип T удовлетворяет определённым требованиям (например, поддерживает копирование или перемещение).
    Контейнеры делятся на последовательные (vector, deque, list, forward_list), ассоциативные (set, map, multiset, multimap) и неупорядоченные (unordered_set, unordered_map). Выбор контейнера — это вопрос сложности операций, локальности данных и гарантий инвалидации итераторов. Например, std::vector обеспечивает O(1) доступ по индексу и кэш-дружелюбное расположение в памяти, но вставка в середину стоит O(n); std::list даёт O(1) вставку в любое место, но доступ по индексу — O(n) и отсутствие локальности.

  • Итераторы — это абстракция указателей, унифицирующая доступ к элементам контейнеров. Они позволяют алгоритмам работать с любым контейнером, не зная его внутреннего устройства. Есть пять категорий итераторов (input, output, forward, bidirectional, random-access), каждая из которых определяет набор поддерживаемых операций. Например, алгоритм std::sort требует random-access итератор — поэтому он работает с vector и deque, но не с list (для которого существует отдельный метод list::sort). Итераторы — это не «просто указатели»: это полноценные объекты, которые могут инкапсулировать логику (например, итераторы потоков ввода-вывода преобразуют символы при чтении).

  • Алгоритмы — это шаблонные функции, принимающие итераторы и выполняющие операции: сортировку, поиск, трансформацию, свёртку и др. Алгоритмы не изменяют структуру контейнера — они работают с его содержимым. Это обеспечивает строгую разделённость ответственности: контейнеры отвечают за хранение, алгоритмы — за обработку. Более того, алгоритмы могут применяться к контейнерам, «сырым» массивам, потокам, пользовательским диапазонам (начиная с C++20 — std::ranges), что делает их исключительно универсальными.

Эта триада даёт разработчику мощный инструментарий, в котором легко заменять одни компоненты на другие без переделки всей логики. Хотите заменить vector на deque — достаточно изменить одну строку объявления, если алгоритмы используют только те операции, которые поддерживают оба контейнера.

Ввод-вывод

Система ввода-вывода в C++ построена на идее потоков — абстракций, моделирующих последовательности данных. Основные классы находятся в заголовке <iostream> и его расширениях (<fstream>, <sstream>):

  • std::istream / std::ostream — базовые классы для чтения и записи.
  • std::cin, std::cout, std::cerr, std::clog — глобальные объекты, связанные с консолью.
  • std::ifstream / std::ofstream / std::fstream — файловые потоки.
  • std::istringstream / std::ostringstream — строковые потоки, используемые для парсинга и форматированного вывода в памяти.

Потоки поддерживают операторы >> (извлечение) и << (вставка), что позволяет писать выразительный код:

int x;
std::cin >> x;
std::cout << "Вы ввели: " << x << "\n";

Важно: потоки — это состоятельные объекты. Ошибка при чтении (например, попытка прочитать число из строки «abc») устанавливает флаг failbit, и все последующие операции игнорируются, пока состояние не будет сброшено. Это позволяет писать надёжный код без исключений (хотя исключения тоже можно включить).

Для тонкой настройки форматирования используются манипуляторы из <iomanip>: std::setw, std::setprecision, std::hex и др. Они временно изменяют поведение потока, не нарушая инкапсуляции.

Управление ресурсами

Если в C управление памятью требует постоянного внимания (malloc/free, new/delete), то в C++ эта ответственность возложена на умные указатели — шаблонные классы, реализующие стратегии владения ресурсами:

  • std::unique_ptr<T> — выражает исключительное владение. Объект принадлежит только одному unique_ptr, и при его уничтожении ресурс освобождается автоматически. Копирование запрещено, но допустимо перемещение. Это самый лёгкий и эффективный умный указатель — накладные расходы отсутствуют по сравнению с «голым» указателем.

  • std::shared_ptr<T> — реализует совместное владение через подсчёт ссылок. Ресурс освобождается, когда счётчик достигает нуля. В отличие от unique_ptr, shared_ptr можно копировать, но это влечёт накладные расходы: атомарные операции при изменении счётчика, дополнительный блок памяти для хранения самого счётчика и, при неосторожности, циклические ссылки (решаются через std::weak_ptr).

Эти классы — не «обёртки для удобства». Они являются воплощением паттерна RAII (Resource Acquisition Is Initialization) — ключевого принципа C++, согласно которому время жизни ресурса (память, файл, сокет, мьютекс) привязано к времени жизни объекта. Это обеспечивает детерминированное освобождение ресурсов даже в случае исключений — чего невозможно достичь вручную без значительного усложнения кода.

Многопоточность

Начиная с C++11, стандартная библиотека предоставляет встроенную поддержку многопоточности — через единый, кроссплатформенный интерфейс:

  • std::thread — инкапсулирует поток выполнения.
  • std::mutex, std::recursive_mutex, std::shared_mutex — примитивы синхронизации.
  • std::lock_guard, std::unique_lock — RAII-обёртки для захвата/освобождения мьютексов.
  • std::condition_variable — для ожидания изменений состояния между потоками.
  • std::future / std::promise — для асинхронной передачи результатов.
  • std::atomic<T> — для lock-free программирования в простых случаях.

Важно: библиотека не скрывает сложность многопоточного программирования — гонки данных, взаимные блокировки, проблемы памяти остаются. Но она предоставляет стандартные, проверенные примитивы, которые не зависят от ОС и могут быть эффективно оптимизированы компилятором.

Прочие ключевые компоненты

  • Работа со строками: std::string и std::string_view (C++17). Первый управляет владением буфером символов, второй — «невладеющим» представлением строки (аналог const char*, но с длиной и безопасностью). Сочетание string + string_view позволяет избегать ненужных копий при передаче строк в функции.

  • Функциональные объекты: std::function, std::bind, лямбда-выражения. Позволяют хранить и передавать callable-объекты (функции, функторы, лямбды) с унифицированным интерфейсом.

  • Обработка ошибок: std::exception, std::runtime_error, std::logic_error и иерархия производных классов. Исключения — часть модели обработки ошибок по умолчанию, хотя в некоторых доменах (встраиваемые системы, ядра ОС) они отключаются.

  • Работа с временем: std::chrono — типобезопасная система измерения времени, заменившая устаревшие time_t и clock().

  • Работа с файловой системой: std::filesystem (C++17) — кроссплатформенный API для манипуляции путями, проверки существования файлов, перебора директорий.

Обратите внимание: почти все эти компоненты не требуют линковки дополнительных библиотек в большинстве реализаций. Они реализованы в заголовках (шаблоны, inline-функции) или встроены в основную библиотеку времени выполнения. Исключение — std::filesystem и иногда std::regex, которые могут требовать явной линковки (например, -lstdc++fs в GCC до версии 9).


Пользовательские библиотеки

Стандартная библиотека — лишь вершина айсберга. Подавляющее большинство реальных проектов полагается на сторонние и внутренние библиотеки: Boost, OpenSSL, Qt, Eigen, fmt, spdlog, и тысячи других. Но даже если вы создаёте небольшую утилиту для внутреннего использования, правильная организация её в виде библиотеки — признак профессионального подхода.

Структура пользовательской библиотеки

Минимальная библиотека состоит из:

  • одного или нескольких заголовочных файлов (.h, .hpp) — интерфейс,
  • одного или нескольких исходных файлов (.cpp) — реализация,
  • (опционально) скрипта сборки (CMakeLists.txt, Makefile и т.д.).

Пример: библиотека math_utils.

math_utils.hpp:

#ifndef MATH_UTILS_HPP
#define MATH_UTILS_HPP

namespace math {
double clamp(double value, double min, double max);
bool is_prime(int n);
}

#endif

math_utils.cpp:

#include "math_utils.hpp"
#include <algorithm>
#include <cmath>

namespace math {
double clamp(double value, double min, double max) {
return std::max(min, std::min(value, max));
}

bool is_prime(int n) {
if (n < 2) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; i <= std::sqrt(n); i += 2) {
if (n % i == 0) return false;
}
return true;
}
}

Обратите внимание: в заголовке — только объявления, в реализации — определения. В заголовке — защита от повторного включения. В реализации — подключение только необходимых стандартных заголовков.

Сборка

Чтобы использовать math_utils в другом проекте, её нужно скомпилировать и скомпоновать.

  1. Статическая библиотека (.a в Unix, .lib в Windows):

    g++ -c math_utils.cpp -o math_utils.o
    ar rcs libmath_utils.a math_utils.o

    Затем при компиляции основной программы:

    g++ main.cpp -L. -lmath_utils -o app

    Результат: код math_utils встраивается в app. Преимущества: проще развёртывание (один файл), выше производительность (возможна агрессивная оптимизация между модулями). Недостатки: увеличение размера исполняемых файлов, невозможность обновления библиотеки без пересборки приложения, дублирование кода при использовании библиотеки в нескольких приложениях.

  2. Динамическая библиотека (.so в Unix, .dll в Windows):

    g++ -fPIC -c math_utils.cpp -o math_utils.o
    g++ -shared -o libmath_utils.so math_utils.o

    При компиляции основной программы:

    g++ main.cpp -L. -lmath_utils -o app

    Но при запуске app система должна найти libmath_utils.so (через LD_LIBRARY_PATH, rpath или установку в системную директорию). Преимущества: общее использование кода, возможность обновления без пересборки приложений, поддержка плагинов. Недостатки: «ад зависимостей», необходимость управления версиями (ABI-совместимость), накладные расходы на загрузку и вызовы через PLT.

Выбор между статической и динамической линковкой — архитектурное решение, зависящее от требований к развёртыванию, безопасности, лицензированию и производительности.

Современные системы управления зависимостями

Ручное управление библиотеками быстро становится неуправляемым. Сегодня используются:

  • CMake — фактический стандарт для описания сборки. Позволяет находить библиотеки (find_package), генерировать файлы экспорта, управлять целями.
  • Conan, vcpkg — менеджеры пакетов для C++. Аналоги npm или pip, но для нативного кода. Они скачивают, собирают и интегрируют зависимости в проект.
  • Hunter, Conda — альтернативные подходы, особенно популярные в научных и embedded-средах.

Эти инструменты берут на себя рутину: проверку версий, разрешение конфликтов, кросс-компиляцию. Они не заменяют понимания того, как работают библиотеки, но позволяют масштабировать проекты до промышленных размеров.


Эволюция модели библиотек

Модель заголовочных файлов, унаследованная от C, оказалась настолько живучей, что пережила смену парадигм — от процедурного программирования к ООП, от однопоточных к параллельным системам. Но её недостатки хорошо известны:

  • Дублирование кода на этапе компиляции: каждый .cpp-файл, включающий <vector>, заставляет компилятор повторно парсить десятки тысяч строк шаблонов. Это — главная причина «header bloat» и роста времени сборки.

  • Зависимость от порядка включения: хотя стандарт требует, чтобы заголовки были идемпотентны (безопасны при многократном включении), в сложных проектах неявные зависимости через #include могут приводить к хрупкому поведению: замена одного заголовка на другой ломает компиляцию — потому, что раньше включался другой заголовок, который сам его подключал.

  • Отсутствие инкапсуляции интерфейса: всё, что объявлено в заголовке, становится доступно пользователю — даже вспомогательные детали реализации. В попытке скрыть их используют pImpl (pointer to implementation) или анонимные пространства имён в .cpp, но это требует дополнительных усилий.

Модули C++20 (import) призваны решить эти проблемы. Модуль — это компилируемая единица, которая экспортирует семантический интерфейс, а не текст. Компилятор один раз анализирует модуль и сохраняет его в двоичном виде (например, .pcm в Clang), после чего потребители могут использовать его без парсинга исходного кода. Это радикально ускоряет сборку и обеспечивает строгую границу между интерфейсом и реализацией.

Однако на 2025 год реальность такова:

  • Поддержка модулей в GCC всё ещё экспериментальна и требует включения флагов;
  • MSVC имеет наиболее зрелую реализацию, но интеграция с крупными legacy-проектами остаётся сложной;
  • Почти все сторонние библиотеки по-прежнему поставляются только в виде заголовков и объектных файлов;
  • Модули не решают проблему распространения: бинарные .pcm-файлы не переносимы между компиляторами и версиями.

Поэтому стратегия большинства промышленных проектов — гидридная:

  • Внутри новых компонентов — экспериментальное использование модулей там, где это оправдано (например, для узких внутренних библиотек);
  • Для интеграции со внешним миром — сохранение совместимости через заголовочные файлы;
  • Использование интерфейсных библиотек (interface libraries в CMake), которые позволяют абстрагировать, как поставляется зависимость — как модуль, как заголовки, как статическая библиотека — без изменения клиентского кода.

Иными словами, модули — это будущее, но #include останется актуальным ещё много лет. Понимание почему так происходит важнее, чем следование моде.


Скрытые ловушки

Знание синтаксиса #include и std::vector не гарантирует корректности. Вот типичные проблемы, возникающие при работе с библиотеками:

Нарушение правила одного определения (ODR violation)

Правило одного определения требует, чтобы любая сущность (класс, функция, шаблон) имела ровно одно определение во всей программе, и все определения были лексически идентичны. Если два .cpp-файла включают разные версии одного заголовка (например, из-за ошибки в путях или условной компиляции), компилятор не увидит конфликта — он работает с translation unit по отдельности. А вот линковщик или, хуже того, рантайм, обнаружит несоответствие: например, один модуль думает, что struct Point имеет два поля, другой — три. Последствия: падения, порча памяти, неопределённое поведение.

Защита:

  • Использовать #pragma once и guard-макросы (#ifndef HEADER_H);
  • Избегать #define в интерфейсах (они глобальны и легко конфликтуют);
  • Для шаблонов — убедиться, что все части определения (включая специализации) находятся в одном заголовке.

Несовместимость двоичного интерфейса (ABI break)

ABI (Application Binary Interface) — это соглашение на уровне машинного кода: как передаются аргументы, где лежат поля в структуре, как реализованы виртуальные таблицы. Изменение любого из этих аспектов ломает совместимость. Например:

  • Добавление/удаление поля в классе (меняет layout);
  • Изменение порядка наследования;
  • Смена компилятора (GCC 9 → GCC 13) или его флагов (включить/выключить _GLIBCXX_USE_CXX11_ABI);
  • Обновление стандартной библиотеки без пересборки зависимостей.

Если приложение линкуется с динамической библиотекой, собранной в несовместимой конфигурации — оно может запуститься, но упасть при первом вызове функции. В статических библиотеках проблема проявляется на этапе линковки (несовпадение имён — name mangling), но не всегда.

Защита:

  • Для публичных API — использовать PIMPL или C-style интерфейс (чистые функции с extern "C");
  • Контролировать версии зависимостей;
  • При поставке бинарных библиотек — документировать ABI-требования (компилятор, стандартная библиотека, флаги).

Зависимость от глобального состояния

Многие библиотеки используют глобальные переменные: std::cout, errno, локали (std::locale), генераторы случайных чисел (std::rand). Это создаёт скрытые связи между модулями. Например, смена локали в одном потоке может повлиять на форматирование в другом — если не используется std::locale::global() аккуратно.

Решение — предпочитать stateless-интерфейсы: передавать локаль явно, использовать std::mt19937 вместо rand(), избегать глобальных синглтонов.


Оптимизация сборки

В проектах с сотнями тысяч строк времени сборки могут измеряться часами. Основные причины — шаблоны и заголовки. Вот проверенные практики:

Предварительно скомпилированные заголовки (PCH)

Идея: один раз скомпилировать тяжёлые, редко меняющиеся заголовки (например, <vector>, <string>, <iostream>), сохранить результат, и переиспользовать его во всех translation unit’ах. В CMake это делается через target_precompile_headers().

Эффект: ускорение на 30–70%, особенно в проектах с большим количеством мелких .cpp-файлов.

Unity builds (Jumbo builds)

Вместо компиляции 100 файлов по отдельности — сгенерировать один .cpp, включающий все остальные:

// unity.cpp
#include "module1.cpp"
#include "module2.cpp"
...

Преимущества:

  • Компилятор видит весь код сразу — возможна межмодульная оптимизация (LTO без флагов);
  • Заголовки парсятся один раз.

Недостатки:

  • Нарушается инкапсуляция (все static и анонимные пространства имён объединяются);
  • Изменение одного файла требует перекомпиляции всего;
  • Ошибки в одном файле могут маскировать другие.

Используется выборочно — например, для модульных тестов или release-сборок.

Интерфейсные библиотеки (CMake)

Цель: отделить описание зависимости от способа её предоставления. Например:

add_library(math INTERFACE)
target_include_directories(math INTERFACE include/)
target_link_libraries(math INTERFACE stdc++fs)

Потребитель делает просто target_link_libraries(app math), и CMake сам добавит пути и линковку. Это позволяет:

  • Менять реализацию (статическая/динамическая/заголовки) без изменения клиентов;
  • Экспортировать зависимости транзитивно;
  • Поддерживать кроссплатформенность (например, на Windows линковать ws2_32, на Linux — ничего).

Это — основа современных CMake-проектов («modern CMake»).


Этические и лицензионные аспекты

Подключение #include <some_lib> — это не только технический акт. Это юридический и этический выбор.

Лицензии

  • MIT, BSD, Apache 2.0 — разрешают использование, модификацию, распространение, в том числе в проприетарных продуктах. Требуют указания авторства (attribution).
  • LGPL — позволяет линковку с проприетарным кодом, но изменения в самой библиотеке должны быть открыты.
  • GPL — «вирусная» лицензия: если вы линкуетесь со статической библиотекой под GPL, ваш код должен быть тоже под GPL (или совместимой лицензией).
  • Proprietary — требуют подписания NDA, платы, ограничений на использование.

Ошибка в оценке лицензии может привести к судебным искам, блокировке продукта, репутационным потерям.

Безопасность и сопровождение

  • Использование устаревшей версии OpenSSL с известными уязвимостями — прямой путь к компрометации.
  • Отсутствие поддержки библиотеки («abandoned project») — риск, когда потребуется срочная правка.
  • Зависимость от одного разработчика («bus factor = 1») — организационная уязвимость.

Прозрачность и подотчётность

В открытых проектах — фиксируйте версии зависимостей (conanfile.txt, vcpkg.json, CMakeLists.txt с хешами). В промышленных системах — проводите аудиты (SCA — Software Composition Analysis), сканируйте на CVE.

Каждая библиотека — это делегирование ответственности. Вы доверяете чужому коду работать от вашего имени — в памяти пользователя, с его данными, в его инфраструктуре. Это требует технической грамотности и профессиональной этики.