Рубрики
Без рубрики

Разделение струн между C и Python через байтовые буферы

Если вы используете буфер байтов для обмена строками между C (или C ++) и Python, вы можете в конечном итоге … Tagged с CPP, C, Python.

Если вы используете буфер байтов для обмена строками между C (или C ++) и Python, вы можете получить странные результаты, потому что оба языка не представляют строки одинаково.

В последнее время я укусил, и вот что я узнал.

C Строки

Если вы использовали C для обработки строк один раз в жизни, то вы столкнулись с одной из самых болезненных вещей в строках C: Null-конце

Строка – это просто указатель в C: по данному адресу вы найдете байты, которые интерпретируются как символы. Как заканчивается строка? Просто когда найден специальный персонаж: '\ 0' (его числовое значение составляет 0). Забудьте об этом символе, и ваша строка заканчивается … Где -то (если программа не рухнет раньше).

Эти струны C также доступны в C ++. Даже если вы, вероятно, предпочтете Используйте std:: string S в C ++ , Есть веские причины придерживаться строк C. Наиболее распространенной причиной является то, что вы не можете использовать динамическое распределение памяти, что почти всегда имеет место в встроенных системах.

Струны Python

Python реализован в C., чтобы быть точным, cpython, справочная реализация , реализация почти все используют, написана в C. Таким образом, вы можете подумать, что потоки Python – это C -струны.

Они не.

Взгляните на StringObject.h от Python’s SVN Анкет

Строка 13, этот комментарий говорит обо всем:

Тип pystringObject представляет строку символов. В конце зарезервирован дополнительный нулевой байт, чтобы убедиться, что он скирован нулевым, но присутствует размер, чтобы можно было представлено строки с нулевыми байтами.

Представление строки в Python отличается и сложнее, чем в C.

Почему это проблема при разделе буфера байтов?

У меня есть устройство IoT с Bluetooth с низкой энергией (BLE) подключением. Bluetooth низкоэнергетические устройства передают данные назад и вперед, используя концепции, называемые «службами» и «характеристики» Анкет Одна из моих характеристик – буфер для обмена строкой: 20 байтов с именем моего устройства.

Прошивка этого устройства записана в C ++, но поскольку динамическое распределение памяти отключено, используются строки C. Поэтому одна строка такого рода записана в буфер.

Мой компьютер запускает программное обеспечение Python, которое подключается к устройству. Он читает 20 байтов и декодирует его как строку.

Из -за различных представлений строки между C (++) и Python я столкнулся с забавными ошибками с мусорными символами в Python.

Код, чтобы проиллюстрировать проблему

Здесь код C ++, который воспроизводит проблему:

#include 
#include 
#include 
#include 

using Characteristic = std::array;

int main() {

    Characteristic device_name{};

    // Write the original name
    const char* original_name = "The original name";
    std::strcpy(device_name.data(), original_name);
    std::cout << device_name.data() << ' ' << std::strlen(device_name.data()) << '\n';

    // Write the new name
    // (this doesn't replace remaining characters with 0s in the buffer)
    const char* new_name = "New name";
    std::strcpy(device_name.data(), new_name);
    std::cout << device_name.data() << ' ' << std::strlen(device_name.data()) << '\n';

    // Mimic serialization over BLE
    // (in fact, this prints a byte literal for the Python code below)
    for (auto c : device_name) {
        std::cout << "\\x" << std::setfill('0') << std::setw(2) << std::hex << static_cast(c);
    }
}

Примечание: Этот код плохой! strcpy Уилл вызвать переполнение буфера Если имена больше 20 девчонка s.

Вывод:

The original name 17
New name 8
\x4e\x65\x77\x20\x6e\x61\x6d\x65\x00\x6e\x61\x6c\x20\x6e\x61\x6d\x65\x00\x00\x00

Как видите, в буфере есть промежуточный байт со значением 0.

Давайте возьмем эти байты и декодируем их с помощью Python:

# Mimic deserialization over BLE
# (just take what the C++ code prints)
raw = b'\x4e\x65\x77\x20\x6e\x61\x6d\x65\x00\x6e\x61\x6c\x20\x6e\x61\x6d\x65\x00\x00\x00'
assert len(raw) == 20

# Get an 'str' object
string = raw.decode('ascii')
print('Decoded: [{}] {}'.format(string, len(string)))

Вывод:

Decoded: [New name nal name   ] 20

Уп! Python не останавливается на первом '\ 0' Итак, имя устройства неверно в моем программном обеспечении Python 😳

Код для решения проблемы

Самое простое и быстрое решение – сделать байт декодирование более надежным в Python. split () может сделать свое дело:

raw = b'\x4e\x65\x77\x20\x6e\x61\x6d\x65\x00\x6e\x61\x6c\x20\x6e\x61\x6d\x65\x00\x00\x00'

string = raw.decode('ascii').split('\0')[0]
print('Decoded and split: [{}] {}'.format(string, len(string)))

Имя устройства правильное тогда:

Decoded: [New name] 8

Другим решением было бы установить все байты с применением на 0 в программе C ++. Точный способ сделать это зависит от вашего фактического кода. Для приведенного выше кода решение может быть использование strncpy :

const char* new_name = "New name";
std::strncpy(device_name.data(), new_name, device_name.size() - 1);

Вывод становится:

\x4e\x65\x77\x20\x6e\x61\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00

strncpy имеет два преимущества по сравнению с strcpy Анкет Во -первых, если new_name Не вписывается в буфер, нет переполнения буфера. Второй, Если new_name меньше размера буфера, все зацепленные байты установлены на 0, как указано в Документация :

Если длина SRC меньше n, strncpy () записывает дополнительные нулевые байты, чтобы убедиться, что написано в общей сложности n байтов.

В общем, лучшее решение – это сделать как сервер, так и клиент более надежным 😅

Редактировать с 1 сентября 2021 года

Почти год спустя я сталкиваюсь с другой ошибкой, связанной с этой темой. Поскольку мой фактический код C ++ не является таким кодом, показанным выше, я полагаюсь только на код Python, чтобы решить эту проблему промежуточного \0 .

Но есть еще одна потенциальная проблема: байты после первого \ 0 не может быть действительным символом ASCII и Декод ('ascii') не удастся. Вот пример:

name = bytearray(b'This a long name\x00+\xed\xb8')
decoded = name.decode('ascii').split('\0')[0]

Ошибка:

UnicoDedeCodeError: «ASCII» Кодек не может декодировать byte 0xed в позиции 18: Органие не в диапазоне (128)

Я должен разделить буфер перед декодированием. Вот возможное решение:

name = bytearray(b'This a long name\x00+\xed\xb8')

index = name.index(0)
decoded = name[0:index].decode('ascii')

Оригинал: “https://dev.to/pgradot/sharing-strings-between-c-and-python-through-byte-buffers-1nj0”