There are many projects that want to make C++ a "safer" programming language. But making changes to the language syntax usually breaks backward compatibility with older code written earlier.
This project contains code for a library header file and compiler plugin for safe C++, which fixes C++'s core problems with memory and reference data types without breaking backwards compatibility with old legacy code.
The global problem of the C and C++ languages is that the pointer to the allocated memory block in the heap is an ordinary address in RAM and has no connection with variables - pointers that are in local variables on the stack, and whose lifetime is controlled by the compiler.
The second, no less serious problem, which often leads to undefined behavior (Undefined Behavior) or data races (Data Races) is access to the same memory area from different threads at the same time.
There are many projects that want to make C++ a "safer" programming language. But making changes to the language's syntax usually breaks backward compatibility with older code written earlier.
This project contains a header only library and a compiler plugin for safe C++, which fixes the main problems of C++ when working with memory and reference data types without breaking backward compatibility with old legacy code (it uses C++20, but can be downgraded to C++17 or C++11).
The method of marking objects in the source code and configuring the plugin's operation parameters is performed using C++ attributes, which is very similar to the security profiles p3038 by Bjarne Stroustrup and P3081 by Herb Sutter, but does not require the creation of a new standard (it is enough to use the existing C++20).
The concept of safe memory management consists of implementing the following principles:
- If the program is guaranteed to have no strong cyclic references (references of an object to itself or cross-references between several objects), then when implementing the RAII principle automatic memory release will be performed always.
- The absence of cyclic references in the program code can only be guaranteed by prohibiting them at the level of types (class definitions).
- The problem of data races when accessing memory from different threads is solved by using inter-thread synchronization objects, and to prevent errors in logic, only one operator (function call) should be used to capture the synchronization object and dereference the reference at the same time in one place.
The concept of safe memory management is ported to C++ from the NewLang language, but is implemented using the standard C++ template classes shared_ptr and weak_ptr.
The main difference when working with reference variables, compared to shared_ptr and weak_ptr, is the method of dereferencing references (getting the address of an object), and the method of accessing the object, which can be done not only by dereferencing the reference "*", but also by capturing (locking) the reference and storing it in a temporary variable, the lifetime of which is limited and automatically controlled by the compiler, and through it direct access to the data itself (the object) is carried out.
Such an automatic variable is a temporary strong reference holder and is similar to std::lock_guard - a synchronization object holder until the end of the current scope (lifetime), after which it is automatically deleted by the compiler.
The implementation of the concept of safe work with memory for C++ consists of two parts: a plugin for Clang and a header file of the library.
Clang plugin performs static analysis of C++ code during its compilation. It checks for invalidation of reference types (iterators, std::span, std::string_view, etc.) when data in the original variable is changed and controls strong cyclic references at the type level (class definitions) of any nesting *).
The library file contains template classes that extend the standard std::shared_ptr and std::weak_ptr
and add automatic data race protection to them when accessing shared variables from different threads
(the access control method must be specified when defining the variable, after which the acquisition
and release of the synchronization object will occur automatically when the reference is renamed).
By default, shared variables are created without multi-thread access control
and have no additional overhead compared to the standard template classes std::shared_ptr
and std::weak_ptr
.
The library header file also contains options for controlling the analyzer plugin (a list of classes that need to be monitored for invalid reference data types is defined).
*) - since C++ compiles files separately, and the class (data structure) definition may be in a different translation unit due to forward declaration, two passes may be required for the circular reference analyzer to work correctly. First run the plugin with the '--circleref-write -fsyntax-only' option to generate a list of classes with strong references, then a second time with the '--circleref-read' option to perform the analysis. Or disable the circular reference analyzer completely with the '--circleref-disable' option.
Command line for compiling a file with clang and using a plugin
clang++ -std=c++20 -Xclang -load -Xclang ./memsafe_clang.so -Xclang -add-plugin -Xclang memsafe -Xclang -plugin-arg-memsafe -Xclang circleref-disable _example.cpp
std::vector<int> vec(100000, 0);
auto x = vec.begin();
vec = {};
vec.shrink_to_fit();
std::sort(x, vec.end()); // malloc(): unaligned tcache chunk detected or Segmentation fault
A fragment of the compiler plugin output with error messages related to invalidation of reference variables after changing data in the main variable:
_example.cpp:29:17: warning: using main variable 'vect'
29 | vect = {};
| ^
_example.cpp:30:17: warning: using main variable 'vect'
30 | vect.shrink_to_fit();
| ^
_example.cpp:31:27: error: Using the dependent variable 'x' after changing the main variable 'vect'!
31 | std::sort(x, vect.end()); // malloc(): unaligned tcache chunk detected or Segmentation fault
| ^
class SharedCross2;
class SharedCross {
SharedCross2 *cross2;
};
class SharedCross2 {
SharedCross *cross;
};
A fragment of the compiler plugin output with error messages when analyzing a class:
_cycles.cpp:53:23: error: The class 'cycles::SharedCross' has a circular reference through class 'cycles::SharedCross2'
53 | SharedCross2 *cross2;
| ^
_cycles.cpp:57:22: error: The class 'cycles::SharedCross2' has a circular reference through class 'cycles::SharedCross'
57 | SharedCross *cross;
| ^
_cycles.cpp:53:23: error: Field type raw pointer
53 | SharedCross2 *cross2;
| ^
_cycles.cpp:53:23: error: The class 'cycles::SharedCross' has a circular reference through class 'cycles::SharedCross2'
_cycles.cpp:57:22: error: Field type raw pointer
57 | SharedCross *cross;
| ^
The plugin's message level can be limited using a macro or command line argument, or after checking the source code, the plugin can be omitted altogether, since it only parses the AST, but does not make any corrections to it.
Show output:
clang++-20 -std=c++26 -ferror-limit=500 -Xclang -load -Xclang ./memsafe_clang.so -Xclang -add-plugin -Xclang memsafe -Xclang -plugin-arg-memsafe -Xclang circleref-disable _example.cpp
...
If you have any suggestions for the development and improvement of the project, join or write.
Глобальная проблема языка C++ в том, что указатель на выделенный блок памяти в куче является обычным адресом оперативной памяти и у него отсутствует связь с локальными переменными - указателями, которые находятся на стеке, и временем жизни которых управляет компилятор.
Вторая, не менее серьезная проблема, которая часто приводить к неопределенному поведению (Undefined Behaviour) или гонке данных (Data Races) - это доступ к одной и той же области памяти из разных потоков одновременно.
Есть много проектов, целью которых является превратить С++ более "безопасный" язык программирования. Но внесение изменений в синтаксис языка обычно нарушает обратную совместимость со старым кодом, который был написан до этого.
Данный проект содержит заголовочный файл библиотеки и плагин компилятора для безопасного С++, который устраняет основные проблемы С++ при работе с памятью и ссылочными типами данных без нарушения обратной совместимости со старым легаси кодом (используется С++20, но можно понизить до С++17 или С++11).
Способ маркировки объектов в исходноом коде и настройка параметров работы плагина выполняется с помощью C++ атрибутов, что очень похоже на профили безопасности p3038 от Bjarne Stroustrup и P3081 от Herb Sutter, но не требует принятия нового стандарта С++.
Концепция безопасной работы с памятью заключается в реализации следующих принципов:
- Если в программе гарантированно отсутствуют сильные циклические ссылки (ссылка объекта на самого себя или перекрестные ссылки между несколькими объектами), тогда при реализации принципа RAII, автоматическое освобождение памяти будет выполнятся всегда без исключений.
- Гарантировать отсутствие циклических ссылок можно только путем их запрета на уровне типов (определений классов).
- Проблема гонок данных при обращении к памяти из разных потоков решается за счет использования объектов межпотоковой синхронизации. Чтобы исключить ошибки в логике для захвата объекта синхронизации и разименования ссылки используются единый оператор (вызов функции).
Изначальная идея безопасной работы с памятью была взята из языка NewLang, но реализована на базе стандартных шаблонных классов С++ shared_ptr и weak_ptr.
Основное отличие новых шаблонов заключается в способе обращения к объекту, который может выполняться не только с помощью разименования "*", но и через захват (блокировку) ссылки с сохранением её во временную переменную, время жизни которой ограничено и автоматически контролируется компилятором, и уже через неё получается доступ непосредственный к самим данным (объекту).
Такая автоматическая переменная является временным владельцем сильной ссылки и выполняет функции удержания объекта межпотоковой синхронизации в стиле std::lock_guard, время жизни которого ограничено текущей областью видимости и управляется компилятором автоматически.
Реализация концепции безопасной работы с памятью для С++ состоит из двух частей: плагина для Clang и заголовочного файла библиотеки.
С помощью плагина для Clang выполняется статический анализ С++ кода во время его компиляции. В плагине реализованы проверка инвалидации ссылочных типов (итераторов, std::span, std::string_view и т.д.) при изменении данных в исходной переменной и контроль сильных циклических ссылок на уровне типов (определений классов) любой вложенности *).
В файле библиотеки находятся шаблонные классы, расширяющие стандартные std::shared_ptr и std::weak_ptr
с автоматической защитой от гонок данных при доступе к общим переменным из разных потоков
(способ контроля доступа требуется указать при определении переменной,
после чего захват и освобождение объекта синхронизации будут происходить автоматически при разименовании ссылки).
По умолчанию общие переменные создаются без контроля многопоточного доступа
и не имеют дополнительных накладных расходов по стравнению с стандартными шаблонными классами std::shared_ptr
и std::weak_ptr
.
Так же в заголовочным файле библиотеки находятся опции для управления плагином анализатора (опредляется список классов, которые необходимо отслеживать для инвалидации ссылочных типов данных).
*) - поскольку C++ компилирует файлы по отдельности, а определение класса (структуры данных) может находиться в другой единице трансляции из-за предварительного объявления, для корректной работы анализатора циклических ссылок может потребоваться два прохода. Сначала запустить плагин с ключом '--circleref-write -fsyntax-only', чтобы сгенерировать список классов с сильными ссылками, затем второй раз с ключом '--circleref-read', чтобы выполнить анализ. Или полностью отключить анализатор циклических ссылок с помощью опции '--circleref-disable'.
Командная строка компиляции файла с помощью clang с загрузкой плагина
clang++ -std=c++20 -Xclang -load -Xclang ./memsafe_clang.so -Xclang -add-plugin -Xclang memsafe -Xclang -plugin-arg-memsafe -Xclang circleref-disable _example.cpp
std::vector<int> vec(100000, 0);
auto x = vec.begin();
vec = {};
vec.shrink_to_fit();
std::sort(x, vec.end()); // malloc(): unaligned tcache chunk detected or Segmentation fault
Фрагмент вывода плагина компилятора с сообщениями об ошибках, связанных с недействительностью ссылочных переменных после изменения данных в основной переменной:
_example.cpp:29:17: warning: using main variable 'vect'
29 | vect = {};
| ^
_example.cpp:30:17: warning: using main variable 'vect'
30 | vect.shrink_to_fit();
| ^
_example.cpp:31:27: error: Using the dependent variable 'x' after changing the main variable 'vect'!
31 | std::sort(x, vect.end()); // malloc(): unaligned tcache chunk detected or Segmentation fault
| ^
class SharedCross2;
class SharedCross {
SharedCross2 *cross2;
};
class SharedCross2 {
SharedCross *cross;
};
Фрагмент вывода плагина компилятора с сообщениями об ошибках при анализе класса:
_cycles.cpp:53:23: error: The class 'cycles::SharedCross' has a circular reference through class 'cycles::SharedCross2'
53 | SharedCross2 *cross2;
| ^
_cycles.cpp:57:22: error: The class 'cycles::SharedCross2' has a circular reference through class 'cycles::SharedCross'
57 | SharedCross *cross;
| ^
_cycles.cpp:53:23: error: Field type raw pointer
53 | SharedCross2 *cross2;
| ^
_cycles.cpp:53:23: error: The class 'cycles::SharedCross' has a circular reference through class 'cycles::SharedCross2'
_cycles.cpp:57:22: error: Field type raw pointer
57 | SharedCross *cross;
| ^
Уровень сообщений плагина можно ограничить с помощью макроса или аргумента командной строки,
либо после проверки исходного кода плагин можно вообще не использовать,
поскольку он только анализирует AST, но не вносит в него никаких исправлений.
Показать вывод
clang++-20 -std=c++20 -ferror-limit=500 -Xclang -load -Xclang ./memsafe_clang.so -Xclang -add-plugin -Xclang memsafe -Xclang -plugin-arg-memsafe -Xclang circleref-disable _example.cpp
...
Если у вас есть предложения по развитию и улучшению проекта, присоединяйтесь или пишите