Начнем с простого:
PROGRAM MAIN VAR A : INT; B : WORD; pA : POINTER TO INT; pB : POINTER TO WORD; dw : DWORD; END_VAR [...] pA := ADR(A); pA^ := -1;
Переменная А в результате будет равна -1. При попытке сделать аналогичное с переменной B — pB^ := -1; получим ошибку компилятора еще на этапе сборки проекта. Объяснение простое — мы объявили pB как указатель на целый и всегда положительный тип WORD, а пытаемся пропихнуть число со знаком, типа INT. Компилятор бдит.
Выход за границу
Вообще, указателям можно присваивать все, что угодно, лишь бы справа был POINTER TO или DWORD (UDINT):
dw := ADR(B);
pB := dw;
pB^ := 123;
ADR возвращает число типа DWORD — это адрес переменной. Поэтому можно присвоить этот адрес указателю pB, а затем разыменовать указатель с помощью оператора ^ и присвоить новое значение для указанной переменной B. После всех действий B будет равен 123.
Оператор ^ применим только к указателям. Нельзя разыменовать переменную другого типа: dw^ := 123; — получим ошибку: '^' needs a pointer type. Поэтому хранить адрес можно в обычных переменных, но работать с адресами получится только через указатели.
Добавим перца — указателю можно присвоить любой адрес или любой другой указатель, указывающий на произвольный тип:
pA := pB; (* pA теперь указывает на переменную 'B' типа WORD *)
pA^ := -2;
Результат: B = 65534 и ошибки здесь нет. Мы объявляли pA, как указатель на целое со знаком, то есть переменную типа INT, а переменная B — это целое беззнаковое, поэтому бит знака превратился в значимый разряд и дальше бла-бла-бла...
До сих пор у нас совпадал размер переменных — обе занимали ровно два байта. Что будет, если мы сделаем так:
A : INT; B : BYTE; [...] pA := pB; (* pA теперь указывает на переменную 'B' типа BYTE *) pA^ := 1234;
B = 210;
...и опять без ошибки, но она может легко возникнуть, так как мы уже вышли за пределы переменной: 1234 занимает в памяти два байта, а мы записываем это число в переменную B типа BYTE, длиной один байт, как нам сообщает Капитан Очевидность. Таким образом легко совершить целый ряд безобразий: вылезти за пределы переменной или массива, вызвать сбой при обращении к странице памяти, остановить контроллер и технологический процесс. Завод встал, рабочие с факелами идут карать Франкенштейна.
Указатель на код
Точнее, на функциональные блоки, ведь на программы ссылаться нельзя.
Для этого эксперимента нам понадобится:
- Глобальная переменная, значение которой будет изменяться функциональными блоками.
- Два ФБ, по разному изменяющие содержимое глобальной переменной.
- Два указателя, которые мы будем испытывать, сталкивая лбами.
- Немного терпения разработчика.
А давайте как физики в эксперименте про квантовую телепортацию — назовем наши функциональные блоки "Алиса" и "Боб" (в алфавитном порядке):
VAR_GLOBAL g_Result : STRING := 'Хзкт'; END_VAR [...] FUNCTION_BLOCK Alice VAR_INPUT Name : STRING; (* пригодится позднее, когда появится злой двойник *) END_VAR g_Result := 'Алиса'; [...] FUNCTION_BLOCK Bob g_Result := 'Боб'; [...] PROGRAM MAIN VAR Al : Alice; Bo : Bob; pAl : POINTER TO Alice; pBo : POINTER TO Bob; END_VAR [...] pAl := ADR(Al); (* Указатель на ФБ Алиса *) pBo := ADR(Bo); (* Указатель на ФБ Боб *) pAl^(); (* ФБ можно вызывать через разыменованный указатель *)
Каждый из ФБ записывает в глобальную переменную g_Result соответствующее имя: в зависимости от того, чей экземпляр мы вызовем через указатель, мы получим разные имена в глобальной переменной. На этот раз, получим результат 'Алиса'.
Попробуем подменить адрес указателя на другой, не обращая внимание, что пытаемся предложить неправильный тип данных:
pAl := ADR(Bo); (* Указатель на ФБ Боб *) pAl^();
И если вы подумали, что получился 'Боб', то вы не правы — g_Result по прежнему равно 'Алиса'. Система следит за типом указателя, не давая сменить его на неправильный, но делает это молча — никому, ни о чем не сообщая.
Давайте создадим второй экземпляр ФБ Alice и переведем указатель на него, то есть присвоим указателю указатель, но теперь правильного типа POINTER TO Alice:
VAR Al : Alice := (Name := 'Alice Original'); AlTwin : Alice := (Name := 'Alice Evil Twin'); [...] pAl := ADR(Al); (* Указатель на ФБ Алиса *) pAl := ADR(AlTwin); (* Указатель на ФБ близнеца Алисы *)
pAl^();
На этот раз, указатель изменился:
TwinCAT 3
Скопируем проект, с небольшими отличиями в объявлении глобальной переменной:
VAR_GLOBAL Result : STRING := 'Хзкт'; END_VAR [...] FUNCTION_BLOCK Alice G.Result := 'Алиса'; (* аналогично для Боба *) [...] pAl := ADR(Al); (* Указатель на ФБ Алиса *) pBo := ADR(Bo); (* Указатель на ФБ Боб *) pAl := pBo; pAl^();
В результате получим — 'Боб', то есть G.Result = 'Боб'. В TwinCAT 3 указатели... э-э-э, гибкие? изменчивые? динамические? отзывчивые?
Нечеловеческий эксперимент над роботами
Жуткий по сложности и непонятности эксперимент над указателями в TwinCAT 2:
- Ссылаемся указателем pA на функциональный блок Alice.
- Получаем адрес функционального блока Bo.
- Получаем группу:смещение указателя pA.
- Через функции ADS записываем (подменяем) адрес указателя pA.
- Разыменовываем указатель pA и вызываем функциональный блок.
Зачем это нужно? Если раньше мы могли предположить, что компилятор как-то там отслеживает все наши махинации при сборке проекта: контролирует и корректирует указатели, то теперь мы те же действия будем делать уже во время работы контроллера. Компилятор о них ничего не узнает:
PROGRAM MAIN VAR ReadSymInfo : PLC_ReadSymInfoByName; SymInfo : SYMINFOSTRUCT; WriteAds : ADSWRITE; Al : Alice; pAl : POINTER TO Alice; Bo : Bob; AddrBob : UDINT; state : UINT; END_VAR [...] CASE state OF 0: pAl := ADR(Al); (* 1 *) AddrBob := ADR(Bo); (* 2 *) state := 100; 100: (* 3 *) ReadSymInfo( NETID := '', PORT := 801, SYMNAME := 'MAIN.pAl', START := TRUE, SYMINFO => SymInfo); IF NOT ReadSymInfoByName.BUSY THEN ReadSymInfoByName(START := FALSE); state := 200; END_IF 200: (* 4 *) WriteAds( NETID := '', PORT := 801, IDXGRP := SymInfo.idxGroup, IDXOFFS := SymInfo.idxOffset, LEN := 4, SRCADDR := ADR(AddrBob), WRITE := TRUE); IF NOT WriteAds.BUSY THEN WriteAds(WRITE := FALSE); state := 300; END_IF 300: pAl^(); (* 5 *) END_CASE
В результате все равно получится 'Алиса' и это несмотря на то, что указатель, судя по адресу, указывает на 'Боба':
В TwinCAT 2 указатели константные, плюс махинации с таблицей символьной информации (SymbolInfo), которая как-то неявно привязана к переменным. Код компилируется раз и навсегда, а вот данные переменных транслируются через специальную таблицу символов и могут изменяться. Указатели стоят где-то на стыке между кодом и переменными, поэтому адрес указателя мы изменить можем, но вызов кода уже не изменим, так как он уже скомпилирован. В общем, указатели в TwinCAT 2 только для данных и ограниченно для кода.
В TwinCAT 3 завезли объектно-ориентированное программирование с классами, методами и, самое главное, интерфейсами. Последние как раз нуждаются в полиморфизме и гибких указателях (vtable). Поэтому с указателями в TwinCAT 3 все ожидаемо-хорошо.
И там, и там можно устроить крах системы, выйдя за пределы выделенной памяти. Осторожнее там с указателями!
Предположения
В TwinCAT 2 указатели константные, плюс махинации с таблицей символьной информации (SymbolInfo), которая как-то неявно привязана к переменным. Код компилируется раз и навсегда, а вот данные переменных транслируются через специальную таблицу символов и могут изменяться. Указатели стоят где-то на стыке между кодом и переменными, поэтому адрес указателя мы изменить можем, но вызов кода уже не изменим, так как он уже скомпилирован. В общем, указатели в TwinCAT 2 только для данных и ограниченно для кода.
В TwinCAT 3 завезли объектно-ориентированное программирование с классами, методами и, самое главное, интерфейсами. Последние как раз нуждаются в полиморфизме и гибких указателях (vtable). Поэтому с указателями в TwinCAT 3 все ожидаемо-хорошо.