При работе с RTTI (Run-Time Type Information) в Delphi иногда возникают неожиданные ошибки доступа к памяти (Access Violation). В данной статье мы разберём конкретный случай с получением значения свойства CaretPosition компонента TMemo через RTTI, который приводит к AV ошибке, а также предложим возможные решения этой проблемы.
Описание проблемы
Рассмотрим следующий код, который пытается получить значение свойства CaretPosition через RTTI:
procedure TFormSimpleDraw2.Button2Click(Sender: TObject);
var
Value: TValue;
LType: TRttiType;
RttiContext: TRttiContext;
CaretPositionProp: TRttiProperty;
CaretPosition: TCaretPosition;
begin
RttiContext := TRttiContext.Create;
LType := RttiContext.GetType(Memo1.ClassInfo);
CaretPosition := Memo1.CaretPosition;
CaretPositionProp := LType.GetProperty('CaretPosition');
Value := CaretPositionProp.GetValue(Memo1); //AV происходит здесь
end;
При выполнении этого кода возникает Access Violation при вызове GetValue. Давайте разберёмся, почему это происходит.
Первое решение: проверка на nil
Первое, что стоит сделать - это добавить проверки на nil для объектов RTTI:
procedure TFormSimpleDraw2.Button2Click(Sender: TObject);
var
Value: TValue;
LType: TRttiType;
RttiContext: TRttiContext;
CaretPositionProp: TRttiProperty;
CaretPosition: TCaretPosition;
begin
RttiContext := TRttiContext.Create;
LType := RttiContext.GetType(Memo1.ClassInfo);
if LType = nil then Exit;
CaretPosition := Memo1.CaretPosition;
CaretPositionProp := LType.GetProperty('CaretPosition');
if CaretPositionProp = nil then Exit;
Value := CaretPositionProp.GetValue(Memo1);
// ...
end;
Однако, как показала практика, проблема не в том, что свойства не найдены (CaretPositionProp не равен nil), а в том, как вызывается геттер этого свойства.
Причина проблемы: несоответствие соглашения о вызовах
Как выяснил Stefan Glienke, корень проблемы в том, что геттер свойства CaretPosition объявлен с соглашением о вызовах cdecl, в то время как RTTI система по умолчанию предполагает стандартное соглашение register.
// Пример объявления свойства с cdecl геттером
property CaretPosition: TCaretPosition read GetCaretPosition cdecl write SetCaretPosition;
RTTI система в Delphi не учитывает соглашение о вызовах при инвокации геттеров и сеттеров свойств, что приводит к ошибке доступа к памяти.
Техническое объяснение
При вызове TRttiProperty.GetValue происходит следующее:
1. Система RTTI находит указатель на метод-геттер
2. Вызывается TRttiInstanceProperty.DoGetValue, который использует System.Rtti.Invoke
3. Внутри Invoke предполагается соглашение register, в то время как реальный геттер использует cdecl
Это несоответствие приводит к неправильной работе стека вызовов и, как следствие, к AV.
Решение от Remy Lebeau
Remy Lebeau открыл баг-репорт в Embarcadero Quality Portal (RSS-3574) с описанием проблемы и предложил следующее временное решение:
Использовать отладчик для изменения параметра CallingConvention в System.Rtti.Invoke с ccReg на ccCdecl
После этого изменения AV исчезает
Это подтверждает, что проблема именно в несоответствии соглашений о вызовах.
Альтернативные решения
Пока проблема не исправлена в самом Delphi, можно предложить несколько обходных путей:
1. Прямой вызов геттера
Если вам нужно только значение CaretPosition, проще использовать прямое обращение:
CaretPosition := Memo1.CaretPosition;
2. Использование обёртки для вызова с правильным соглашением
Можно создать специальную функцию для вызова методов с соглашением cdecl:
function GetCDeclPropertyValue(Instance: TObject; Prop: TRttiProperty): TValue;
var
Method: TRttiMethod;
Addr: Pointer;
begin
Method := (Prop as TRttiInstanceProperty).PropInfo.Getter;
Addr := Method.CodeAddress;
// Здесь должна быть реализация вызова с соглашением cdecl
// Это требует низкоуровневой работы с указателями и стеком
// и может быть нестабильным решением
// В реальном коде это будет сложнее, чем показано здесь
Result := TValue.From<TCaretPosition>(PCaretPosition(Addr)^);
end;
3. Использование интерфейсов вместо RTTI
Если возможно, создайте интерфейс с нужными свойствами и работайте через него:
type
IMemoHelper = interface
function GetCaretPosition: TCaretPosition;
property CaretPosition: TCaretPosition read GetCaretPosition;
end;
// Реализация через класс-помощник
4. Ожидание исправления от Embarcadero
Следите за обновлениями Delphi и исправлением бага RSS-3574.
Пример безопасного кода
Вот более безопасная версия исходного кода с обработкой возможных ошибок:
procedure TFormSimpleDraw2.Button2Click(Sender: TObject);
var
Value: TValue;
LType: TRttiType;
RttiContext: TRttiContext;
CaretPositionProp: TRttiProperty;
begin
try
RttiContext := TRttiContext.Create;
try
LType := RttiContext.GetType(Memo1.ClassInfo);
if LType = nil then
begin
ShowMessage('Не удалось получить тип для TMemo');
Exit;
end;
CaretPositionProp := LType.GetProperty('CaretPosition');
if CaretPositionProp = nil then
begin
ShowMessage('Свойство CaretPosition не найдено');
Exit;
end;
try
Value := CaretPositionProp.GetValue(Memo1);
// Работа с Value
except
on E: Exception do
ShowMessage('Ошибка при получении значения: ' + E.Message);
end;
finally
RttiContext.Free;
end;
except
on E: Exception do
ShowMessage('Общая ошибка: ' + E.Message);
end;
end;
Заключение
Проблема с AV при вызове TRttiProperty.GetValue для свойства CaretPosition в TMemo связана с несоответствием соглашений о вызовах между RTTI системой и реальным геттером свойства. Это известная проблема (RSS-3574), и её окончательное решение должно прийти с обновлениями Delphi.
До тех пор рекомендуется:
1. Использовать прямое обращение к свойству вместо RTTI, если это возможно
2. Быть осторожным при работе с RTTI для свойств, которые могут использовать нестандартные соглашения о вызовах
3. Следить за обновлениями Delphi и исправлением данного бага
Работа с RTTI в Delphi - мощный инструмент, но, как видно из этого примера, требует понимания внутренних механизмов и осторожности при использовании.
Ошибка AV при работе с TRttiProperty.GetValue в Delphi возникает из-за несоответствия соглашений о вызовах между RTTI системой (ожидающей register) и реальным геттером свойства (использующим cdecl), что приводит к неправильной работе стека.
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.