PDA

Просмотр полной версии : [Урок] Почему не следует возвращать строки/массивы напрямую



Daniel_Cortez
17.08.2019, 20:37
Всем привет.

Раньше, когда люди спрашивали, чем плохо то, что функция возвращает строку, основным (и самым очевидным) доводом было лишнее копирование данных: т.е. сначала строка/массив копируется в стек при возврате значения из функции, затем ещё раз копируется из стека в другой массив, к которому идёт присваивание значения. Но, внезапно, для некоторых ленивых скриптеров это не аргумент...
Не так давно получилось найти новый изъян, который (может быть) переубедит ещё несколько скриптеров: баг с возвратом массивов, который может привести к неправильной работе сервера.

Допустим, у нас есть следующие две функции:

new global_string[12] = {"Hello world"};

StringOrigin() {
return global_string;
}

ReturnString() {
return StringOrigin();
}
Довольно просто, так ведь? Есть глобальная строка, и есть функция, которая возвращает эту строку. И ещё есть другая функция, которая возвращает результат первой функции.



new local[12];
strcat(local, StringOrigin(), sizeof(local));
print(local);
Такой код работает вполне нормально. Нативная функция strcat() вызывает Pawn-функцию StringOrigin(), которая просто возвращает глобальную строку. Это первый "уровень" вложенности возврата массивов (strcat() -> StringOrigin()).



new local[12];
strcat(local, ReturnString(), sizeof(local));
print(local);
А вот такой код уже не работает. strcat() вызывает функцию ReturnString(), которая возвращает то, что ей возвращает StringOrigin(). Это второй "уровень" вложенности (strcat() -> ReturnString() -> StringOrigin()), и на нём уже проявляется ошибка.
При подключенном плагине CrashDetect выводится следующее сообщение:

[debug] Run time error 5: "Invalid memory access"


Решение проблемы очень простое: всегда возвращайте строки через массив, переданный по ссылке.

new string[12] = {"Hello world"};

StringOrigin(output[], size = sizeof output) {
strcat(output, string, size);
return 0; // Будем считать, что 0 - код успешного выполнения этой функции.
}

ReturnString(output[], size = sizeof output) {
return StringOrigin(output, size); // Нет ничего плохого в дальнейшем возврате возвращаемого значения
// из StringOrigin(), т.к. это всего лишь одна ячейка (0), а не массив.
}



Оригинал примера с возвратом строк: https://github.com/sampctl/pawn-array-return-bug
Перевод и дополнение: Daniel_Cortez (http://pro-pawn.ru/member.php?100-Daniel_Cortez)

Специально для Pro-Pawn.ru (http://www.pro-pawn.ru)
Копирование данной статьи на других ресурсах без разрешения автора запрещено!

Alpano
22.08.2019, 20:15
Простите, недогоняю вообще.
Появилисъ проблемы с функцией:


stock FixSQLiteRusText(const text[], len,needlow = 1){
new textEx[256];
strcat(textEx,text,len+1);
strdel(textEx,strlen(textEx),len+1);
for(new f = 0; f <= strlen(textEx); f++){
if(!textEx[f]) textEx[f] = text[f];
if(-65 < textEx[f] < 0)
textEx[f]+=256;
if(needlow && textEx[f] == '_') textEx[f] = ' ';
}
return textEx;
}
Приходится юзатъ с массивами которые содержат кириллицу из БД. (SQLite):

new buffer[70];
db_get_field_assoc(u_result, "bNick", buffer,70);
strcat(string,FixSQLiteRusText(buffer,strlen(buffer)));
прошу прощения если быдлокод, но увы.
помогите пожалуйста :)

сам трабл:

[17:50:08] [debug] Run time error 3: "Stack/heap collision (insufficient stack size)"
[17:50:08] [debug] Stack pointer (STK) is 0x3256D4, heap pointer (HEA) is 0x325810
[17:50:08] [debug] AMX backtrace:
[17:50:08] [debug] #0 0024595c in FixSQLiteRusText (text[]=@00325f10 "OnlyCjeat", len=9, needlow=1) at drift.pwn:5422

DeimoS
28.08.2019, 23:54
Так как название темы довольно говорящее и, скорее всего, на него будут натыкаться и те, кто не знаком с основными проблемами возврата строк, стоит отметить так же и то, что при лишнем копировании данных увеличивается количество потребляемого стека на каждый неоднократный вызов функции.
(пояснение для тех, кто лишнее копирование данных может рассматривать именно со стороны лишнего действия, а не лишнего потребления памяти)

То бишь, в этом примере:
native printf(const format[], {Float,_}:...);
new global_string[12] = {"Hello world"};

main()
{
printf("%s", StringOrigin());
}


StringOrigin() {
return global_string;
}
Размер занятого стека будет равен где-то 20 ячейкам.

А вот в этом примере:
native printf(const format[], {Float,_}:...);
new global_string[12] = {"Hello world"};

main()
{
printf("%s %s", StringOrigin(), StringOrigin());
}


StringOrigin() {
return global_string;
}
Размер занятого стека возрастёт где-то на 13 ячеек (12 на хранение содержимого массива + 1 ячейка на хранение адреса функции StringOrigin) и станет равным 33-ём. И если мы добавим ещё один такой вызов внутрь printf, размер стека увеличится ещё на 13 ячеек. И так далее.

При этом, если вызывать глобальный массив напрямую, то, сколько бы раз подряд вы его не вызывали, его содержимое не будет копироваться в стек и никакого подобного "раздутия" происходить не будет.
Если уж очень хочется, чтоб код вызова массива выглядел как код вызова функции, то либо используйте вариант, показанный Daniel_Cortez (передавайте массив по ссылке), либо используйте макросы:
new global_string[12] = {"Hello world"};
#define StringOrigin() global_string
// И в последующем коде можно спокойно использовать "StringOrigin()", не боясь за "раздутие" стека

vvw
29.08.2019, 21:41
Вообще странно, что язык существует уже столько лет, а люди так и не придумали обход для этой проблемы. Можно было бы написать какую-нибудь библиотеку, которая будет работать через байт-код. Строка будет указателем на первую ячейку строки в памяти. Выделять строку можно через инструкции (либо использовать y_malloc). Удалять же можно автоматический, например, через "деконструкторы" (благо такие, насколько мне известно, имеются в pawn уже давно), либо через функцию. Такие строки можно и сравнивать и быстро переприсваивать.

Daniel_Cortez
30.08.2019, 09:46
либо используйте макросы:
new global_string[12] = {"Hello world"};
#define StringOrigin() global_string
// И в последующем коде можно спокойно использовать "StringOrigin()", не боясь за "раздутие" стека
Маскируясь под вызов функции, такой код всё ещё будет создавать "рекламу" обычному возврату через стек. ИМХО, такая себе полумера =/


Вообще странно, что язык существует уже столько лет, а люди так и не придумали обход для этой проблемы. Можно было бы написать какую-нибудь библиотеку, которая будет работать через байт-код. Строка будет указателем на первую ячейку строки в памяти. Выделять строку можно через инструкции (либо использовать y_malloc). Удалять же можно автоматический, например, через "деконструкторы" (благо такие, насколько мне известно, имеются в pawn уже давно), либо через функцию. Такие строки можно и сравнивать и быстро переприсваивать.
Есть кое-что похожее, в виде плагина: https://github.com/IllidanS4/PawnPlus

DeimoS
30.08.2019, 10:31
Маскируясь под вызов функции, такой код всё ещё будет создавать "рекламу" обычному возврату через стек. ИМХО, такая себе полумера =/

Так чтоб подобного не было, нужно везде, где подобный способ указывается, уточнять, что возврат через стек, при неправильном использовании - зло. Те, кому нет дела до проблем с возвратом стека, будут его использовать даже если использование макросов возвести в ранг табу. Для остальных же лучше объяснить как пользоваться инструментом правильно, а не замалчивать его существование только потому, что при неправильном использовании это может создать проблемы. Собственно, в своём дополнении я и пытался акцентировать внимание на правильном использовании.
Ну это моё ИМХО, естественно.

Daniel_Cortez
06.09.2020, 20:38
На днях пофиксил этот баг; исправление предложено (https://github.com/pawn-lang/compiler/pull/567) к включению принято в компилятор и уже доступно в последнем неофициальном релизе (https://pro-pawn.ru/showthread.php?2207&p=97104&viewfull=1#post97104).

Думаю, после выхода компилятора версии 3.10.11 точно можно будет закрыть эту тему за неактуальностью.