Как сделать два в противном случае идентичных типа указателей несовместимыми
В некоторых архитектурах может потребоваться использование разных типов указателей для идентичных в других отношениях объектов. В частности, для процессора Гарвардской архитектуры вам может понадобиться что-то вроде:
uint8_t const ram* data1;
uint8_t const rom* data2;
В частности, именно так выглядело определение указателей на ПЗУ / ОЗУ в MPLAB C18 (в настоящее время прекращено) для PIC. Это может определить даже такие вещи, как:
char const rom* ram* ram strdptr;
Что означает указатель в ОЗУ на указатели в ОЗУ, указывающие на строки в ПЗУ (используя ram
не является необходимым, так как по умолчанию все это находится в оперативной памяти этого компилятора, просто добавлено все для ясности).
Хорошая вещь в этом синтаксисе состоит в том, что компилятор способен предупреждать вас, когда вы пытаетесь назначить несовместимым образом, например, адрес места в ПЗУ для указателя на ОЗУ (так что-то вроде data1 = data2;
или передача указателя ПЗУ в функцию с использованием указателя ОЗУ вызовет ошибку).
В отличие от этого, в avr-gcc для AVR-8 нет такого типа безопасности, поскольку он скорее предоставляет функции для доступа к данным ПЗУ. Нет способа отличить указатель на RAM от указателя на ROM.
Существуют ситуации, когда такого рода безопасность типов была бы очень полезна для выявления ошибок программирования.
Есть ли какой-нибудь способ добавить аналогичные модификаторы к указателям каким-либо образом (например, с помощью препроцессора, расширяющегося до чего-то, что могло бы имитировать это поведение) для этой цели? Или даже что-то, что предупреждает о неправильном доступе? (в случае avr-gcc, попытка получить значения без использования функций доступа к ПЗУ)
4 ответа
Поскольку я получил несколько ответов, которые предлагают различные компромиссы при предоставлении решения, я решил объединить их в один, описав преимущества и недостатки каждого из них. Таким образом, вы можете выбрать наиболее подходящий для вашей конкретной ситуации
Именованные адресные пространства
Для конкретной проблемы решения этой проблемы, и только в этом случае указателей ПЗУ и ОЗУ на микроконтроллере AVR-8, наиболее подходящим решением является следующее.
Это было предложение для C11, которое не вошло в окончательный стандарт, однако есть компиляторы C, которые его поддерживают, включая avr-gcc, используемый для 8-битных AVR.
С соответствующей документацией можно ознакомиться здесь (часть онлайнового руководства по GCC, включая другие архитектуры, использующие это расширение). Рекомендуется по сравнению с другими решениями (такими как функциональные макросы в pgmspace.h для AVR-8), поскольку при этом компилятор может выполнять соответствующие проверки, в то время как в противном случае доступ к указанным данным остается ясным и простым.
В частности, если у вас есть похожая проблема переноса чего-либо из компилятора, который предлагает какие-то именованные адресные пространства, такие как MPLAB C18, это, вероятно, самый быстрый и чистый способ сделать это.
Портированные указатели сверху будут выглядеть следующим образом:
uint8_t const* data1;
uint8_t const __flash* data2;
char const __flash** strdptr;
(Если возможно, можно упростить процесс, используя соответствующие определения препроцессора)
(Оригинальный ответ Олафа)
Структурная инкапсуляция, указатель внутри
Этот метод направлен на усиление типирования указателей, оборачивая их в структуры. Предполагаемое использование состоит в том, что вы передаете сами структуры через интерфейсы, с помощью которых компилятор может выполнять проверки типов на них.
Тип указателя на байтовые данные может выглядеть так:
typedef struct{
uint8_t* ptr;
}bytebuffer_ptr;
К указанным данным можно получить доступ следующим образом:
bytebuffer_ptr bbuf;
(...)
bbuf.ptr = allocate_bbuf();
(...)
bbuf.ptr[index] = value;
Прототип функции, принимающий такой тип и возвращающий его, может выглядеть следующим образом:
bytebuffer_ptr encode_buffer(bytebuffer_ptr inbuf, size_t len);
(Оригинальный ответ от двхх)
Структурная инкапсуляция, указатель снаружи
Подобно методу, описанному выше, он направлен на усиление типизации указателей, заключая их в структуры, но другим способом, обеспечивая более надежное ограничение. Тип данных, на которые нужно указать, это инкапсулированный.
Тип указателя на байтовые данные может выглядеть так:
typedef struct{
uint8_t val;
}byte_data;
К указанным данным можно получить доступ следующим образом:
byte_data* bbuf;
(...)
bbuf = allocate_bbuf();
(...)
bbuf[index].val = value;
Прототип функции, принимающий такой тип и возвращающий его, может выглядеть следующим образом:
byte_data* encode_buffer(byte_data* inbuf, size_t len);
(Оригинальный ответ от Лундина)
Какой я должен использовать?
Именованные адресные пространства в этом отношении не требуют особого обсуждения: они являются наиболее подходящим решением, если вы хотите иметь дело только с особенностями вашей целевой обработки адресных пространств. Компилятор предоставит вам необходимые проверки во время компиляции, и вам не нужно пытаться что-то придумывать дальше.
Однако, если по другим причинам вы заинтересованы в переносе структуры, это вопросы, которые вы можете рассмотреть:
Оба метода могут быть оптимизированы просто отлично: по крайней мере, GCC будет генерировать идентичный код от одного до использования простых указателей. Так что вам не нужно учитывать производительность: они должны работать.
Указатель внутри полезен, если у вас есть сторонние интерфейсы для обслуживания, которые требуют указателей, или, может быть, если вы реорганизуете что-то настолько большое, что вы не можете сделать за один проход.
Указатель снаружи обеспечивает более надежную безопасность типов, так как вы усиливаете сам указательный тип им: у вас есть действительно отличный тип, который вы не можете легко (случайно) преобразовать (неявное приведение).
Указатель снаружи позволяет использовать модификаторы на указателе, такие как добавление
const
, что важно для создания надежных интерфейсов (вы можете сделать данные, предназначенные для чтения только функциейconst
).Имейте в виду, что некоторым людям может не понравиться ни один из них, поэтому, если вы работаете в группе или создаете код, который может повторно использоваться известными сторонами, сначала обсудите этот вопрос с ними.
Должно быть очевидно, но имейте в виду, что инкапсуляция не решает проблему необходимости специального кода доступа (например, с помощью макросов pgmspace.h на AVR-8), предполагая, что именованные адресные пространства не используются вместе с методом. Он предоставляет только метод для выдачи ошибки компиляции, если вы пытаетесь использовать указатель функциями, работающими в другом адресном пространстве, чем то, на которое он намерен указывать.
Спасибо за все ответы!
Одна хитрость заключается в том, чтобы обернуть указатели в структуру. Указатели на структуру имеют лучшую безопасность типов, чем указатели на примитивные типы данных.
typedef struct
{
uint8_t ptr;
} a_t;
typedef struct
{
uint8_t ptr;
} b_t;
const volatile a_t* a = (const volatile a_t*)0x1234;
const volatile b_t* b = (const volatile b_t*)0x5678;
a = b; // compiler error
b = a; // compiler error
Вы можете инкапсулировать указатель в другую структуру для RAM и ROM, что делает тип несовместимым, но содержит значения того же типа.
struct romPtr {
void *addr;
};
struct ramPtr {
void *addr;
};
int main(int argc, char **argv) {
struct romPtr data1 = {NULL};
struct romPtr data3 = data1;
struct ramPtr data2 = data1; // <-- gcc would throw a compilation error here
}
Во время компиляции:
$ cc struct_test.c
struct_test.c: In function ‘main’:
struct_test.c:12:24: error: invalid initializer
struct ramPtr data2 = data1;
^~~~~
Можно конечно typedef
s структура для краткости
Истинные гарвардские архитектуры используют разные инструкции для доступа к различным типам памяти, таким как код (Flash на AVR), данные (RAM), аппаратные периферийные регистры (IO) и, возможно, другие. Значения адресов в диапазонах обычно перекрываются, т. Е. Одно и то же значение обращается к разным внутренним устройствам в зависимости от инструкции.
Возвращаясь к C, если вы хотите использовать унифицированный указатель, это означает, что вам нужно не только кодировать адрес (значение), но и тип доступа (далее "адресное пространство") в значении указателя. Это можно сделать, используя дополнительные биты в значении указателя, но также выбрать соответствующую инструкцию во время выполнения для каждого доступа. Это создает значительные накладные расходы для сгенерированного кода. Кроме того, часто в "естественном" значении отсутствуют запасные биты по меньшей мере для некоторых адресных пространств (например, все 16 битов указателя уже используются для адреса). Поэтому требуются дополнительные биты, по крайней мере, в байтах. Это также увеличивает использование памяти (в основном ОЗУ).
И то, и другое обычно неприемлемо для типичных MCU, использующих эту архитектуру, поскольку они уже весьма ограничены. К счастью, для большинства приложений абсолютно необязательно (или, по крайней мере, легко избежать) определение адресного пространства во время выполнения.
Чтобы решить эту проблему, все компиляторы для такой платформы поддерживают какой-либо способ сообщить компилятору, в котором находятся адресное пространство и объект. В стандартном проекте N1275 для последующего C11 предлагается стандартный способ с использованием "именованных адресных пространств". К сожалению, он не попал в финальную версию, поэтому у нас остались расширения компилятора.
Для gcc (см. Документацию для других компиляторов) разработчики реализовали оригинальное стандартное предложение. Так как адресные пространства зависят от цели, код не переносим между различными архитектурами, но, как правило, это верно для встроенного кода с нуля, в любом случае ничего не потеряно.
При чтении документации для AVR адресное пространство просто используется аналогично стандартному классификатору. Компилятор автоматически выдаст правильные инструкции для доступа к правильному пространству. Также есть единое адресное пространство, которое определяет область во время выполнения, как объяснено выше.
Адресные пространства работают аналогично классификаторам, существуют более строгие ограничения для определения совместимости, т. Е. При назначении указателей разных адресных пространств друг на друга. Подробное описание см. В предложении, глава 5.
Заключение:
именованные адресные пространства - это то, что вы хотите. Они решают две проблемы:
- Убедитесь, что указатели на несовместимые адресные пространства не могут быть назначены друг другу незамеченными.
- Сообщите компилятору, как получить доступ к объекту, т.е. какие инструкции использовать.
Что касается других ответов, предлагающих struct
s, вы должны указать адресное пространство (и тип для void *
) в любом случае, как только вы получите доступ к данным. Назначение адресного пространства в объявлении сохраняет остальную часть кода в чистоте и даже позволяет изменить его позже в одном месте исходного кода.
Если вам нужна переносимость между цепями инструментов, прочитайте их документацию и используйте макросы. Скорее всего, вам просто нужно будет принять фактические имена адресных пространств.
Sidenote: Пример PIC18, который вы цитируете, фактически использует синтаксис для именованных адресных пространств. Просто имена устарели, потому что реализация должна оставлять все нестандартные имена свободными для кода приложения. Отсюда и подчеркивание имен в gcc.
Отказ от ответственности: я не тестировал функции, но полагался на документацию. Полезные отзывы в комментариях приветствуются.