Тем не менее, я обнаружил (хоть и не первым1) ещё один трюк (см. ниже), который позволит приблизиться по удобству к Си тем, кому необходим простой доступ памяти.
Предположим, что p — указатель (адрес).
Тогда получение (чтение) значения по этому адресу будет выглядеть так: val = *p.
А присвоение (запись) значения по этому адресу так: *p = val.
Унарный оператор * в соответствие указателю ставит то, на что он указывает. А выражение с ним является так называемым l-value.
- На заметку:
В программирование термином l-value в императивных языках называют выражение, которому можно присвоить значение, то есть выражение, которое может стоять слева (left, отсюда и название) от оператора присваивания (= в VB и Си, := в Паскале). К l-value, как правило можно, применить оператор &, который вернёт адрес l-value.
p может быть не указателем на данные, а указателем на указатель на данные.
Тогда получение (чтение) значения по этому адресу будет выглядеть так: val = **p.
А присвоение (запись) значения по этому адресу так: **p = val.
Антагонистом оператора * является оператор &: он возвращает не то, что по указанному адресу, а адрес того, что указали. Эти два оператора «взаимоуничтожают» эффект друг-друга: взаимоотношения между ними такие же, как между синусом и арксинусом, квадратным корнем и возведением в квадрат.
Есть ли в VB что-то подобное?
- Получить указатель/адрес легко: аналогом сишного & можно назвать фиктивную2 функцию VarPtr.
- Получить (прочитать) или присвоить (записать) значение по указателю/адресу на порядок сложнее. Ничего похожего на оператор * мы не имеем. Давно открыты недокументированные функции GetMemN (чтобы прочитать) и PutMemN, позволяющие читать и писать по имеющемуся адресу. Но эти функции по элегантности использования не могут сравниться с оператором *, хотя бы тем, что это две разные функции, а оператор * — один!
Для примера код на Си с двухуровневыми указателями:- Код: Выделить всё
**to = *(*(from)); // Скобки поставлены для тех, кто плохо знаком с указателями в Си.
Вот как это же действие выполняется на VB с применением GetMem/PutMem:- Код: Выделить всё
Dim tmp As Long
Dim tmp2 As Long
GetMem4 from, tmp ' Соответствует подвыражению «*(from)»
GetMem4 tmp, tmp ' Соответствует подвыражению «*(*(from))»
GetMem4 to, tmp2 ' Соответствует подвыражению «*to»
PutMem4 tmp2, tmp ' Соответствует выражению «**to = **from»
Куча строк вместо одного выражения, плюс необходимость в дополнительный переменных. Очень некрасиво.
Хитрость
Занимаясь исследованием экспортируемых функций vba6.dll, я обратил внимание, что очень многие функции существуют парами (EbGetЧтоНибудь и EbSetЧтоНибудь) и семантика вызова характерна COM: актуальным возвращаемым значением является HRESULT (код статуса), а формальное возвращаемое значение передаётся через последний параметр-ссылку. Кроме того, в сумме семантика вызова для Get- и Set-функций очень уж походила на семантику вызова Get- и Let-хендлеров свойств (Property).
В моём коде EbGetXxxx/EbSetXxxxx использовались вперемешку с GetMemX/PutMemX, и тут меня осенило:
GetMemX и PutMemX — это не просто недокументированные функции, это Get-хендлер и Let-хендлер некоего свойства!
Я напоминаю, что VB (COM) позволяет вам объявлять свойства не только в классах, но и в обычных модулях! И этот как раз тот самый случай: модульные свойства со своими обработчиками. А ещё свойства могут принимать параметр (любого типа).
А ещё у свойств есть очень интересное, особенно в контексте данного разговора, качество — свойство является lvalue, потому что ему можно присвоить значения.
В ту же минутe была написана TLB-шка с объявлением модульного Public-свойства «At», реализациями обработчиков присвоения и получения которого были назначены функции PutMemX и GetMemX из MSVBVM.
Что нам это даёт?
А то, что мы имеем синтаксически эквивалентный аналог сишным оператором * и &.
Несколько аналогий:
- Код: Выделить всё
Си: VB:
var = *addr; var = At(addr)
*addr = var; At(addr) = var
*pNum = 123; At(pNum) = 123
*pDest = *pSrc + 17; At(pDest) = At(pSrc) + 17
*(*(*(a))) = ***b; At(At(At(a))) = At(At(At(b)))
*(&(var)) экв var At(VarPtr(var)) экв var
foo(*px, *py) foo(At(px), At(py))
Для тех, кто мало или вообще не знаком с Си и слабо понимает вышенаписанное, простые примеры:
- Читаем (и выводим) значение по адресу 400: MsgBox At(400).
- Присваиваем (записываем) число 123 по адресу 16: At(16) = 123.
- Читаем значение по адресу 4, умножаем его на семь, записываем по адресу 8: At(8) = At(4) * 7.
- Присвоение вида a = b с выкрутасами: At(VarPtr(a)) = At(VarPtr(b)).
Особенности:
В Си операндом оператора * должно быть выражение указательного типа (степень индирекции больше нуля). Оператор * сохраняет базовый тип, уменьшая на единицу степень индирекции типа выражения. У нас же псевдооперандом псевдооператора At является Long-значение, а типом возврата будет вполне конкретный тип. Из этого вытекаеют следующие особенности:
- Объявляено три свойства: At, AtW, AtB — для типов Long, Integer и Byte. Названия даны по аналогии с AscW и AscB.
- *(x+1) — это всегда значение элемента, отстающего от *x на один элемент.
At(x + 1) — всегда значение элемента, отстающего от At(x) на один байт. - VarPtr в частности, и автоматическое приведение типов вообще, имеющее место быть при передачи чего-либо By Ref, имеет отличие от оператора & в Си. Поэтому корректное в Си выражение &(*(foo)), эквивалентное просто выражению foo, в нашем случае не будет эквивалентным: VarPtr(At(address)) не вернёт address.
Как?
Очень просто, изготавливаете новую или вставляете в существующую библиотеку типов модуль со следующим кодом:
- Код: Выделить всё
[dllname("msvbvm60.dll")]
module Memory
{
[entry("GetMem4"), propget] HRESULT _stdcall At( [in]long Address, [out, retval]long* rv);
[entry("GetMem4"), propget] HRESULT _stdcall At_([in]void* Address, [out, retval]long* rv);
[entry("PutMem4"), propput] HRESULT _stdcall At( [in]long Address, [in]long nv);
[entry("PutMem4"), propput] HRESULT _stdcall At_([in]void* Address, [in]long nv);
[entry("GetMem2"), propget] HRESULT _stdcall AtW([in]long Address, [out,retval]short* rv);
[entry("PutMem2"), propput] HRESULT _stdcall AtW([in]long Address, [in]short nv);
[entry("GetMem2"), propget] HRESULT _stdcall AtW_([in]void* Address, [out,retval]short* rv);
[entry("PutMem2"), propput] HRESULT _stdcall AtW_([in]void* Address, [in]short nv);
[entry("GetMem1"), propget] HRESULT _stdcall AtB([in]long Address, [out, retval]unsigned char* rv);
[entry("PutMem1"), propput] HRESULT _stdcall AtB([in]long Address, [in]unsigned char nv);
[entry("GetMem1"), propget] HRESULT _stdcall AtB_([in]void* Address, [out, retval]unsigned char* rv);
[entry("PutMem1"), propput] HRESULT _stdcall AtB_([in]void* Address, [in]unsigned char nv);
}
Здесь я добавил ещё вариант с символом подчёркивания — у него обязательный параметр свойства будет иметь тип As Any, что позволит в качестве адреса указывать всё, что угодно, минуя VarPtr.
Те же, кого смущает само название «At», могут переименовать его в «MemAt» или «AtAddress» или «MemAtAddress» или как вам ещё будет угодно.
Возможно я сделаю свою TLB и выложу её в «кирпичный завод» в недалёком будущем.
Сноски: