AnyEvent сломан на Strawberry Perl 5.20

Темы:

В конце прошлой недели рассылка perl5-porters опять наполнились проклятиями Марка Леманна из-за очередной поломки обратной совместимости в Perl5. Как выяснилось AnyEvent оказался сломан на платформе Windows из-за несовместимых изменений, который произошли в версии 5.19.4.

Как известно, POSIX регламентирует стандарт наименования кодов ошибок, чтобы облегчить создание более переносимых программ. Все подобные коды ошибок определены в заголовочном файле errno.h стандартной библиотеки C. Но как известно существуют и не-POSIX платформы, которые определяют свои коды ошибок, например, на системе Windows в errno.h присутствуют ошибки с префиксом WSA, например WSAEINVAL, которая возвращается, если передано недопустимое значение. Данная ошибка по смыслу соответствует стандартному EINVAL.

Чтобы облегчить создание переносимых программ многие реализации скриптовых языков и, в том числе Perl, производят преобразование WSAE* кодов в соответствующие E* коды, таким образом, программа проверяющая, например, только код ошибки EINVAL будет работать как на POSIX-платформе, так и на Windows. К сожалению, не все нужные E* коды присутствовали в errno.h компилятора Visual C++ для подобных преобразований. Поэтому в Perl долгое время искусственно добавляли недостающие E* коды с номером > 100. Всё изменилось с выходом VC+2010, где отсутствующие коды появились и стали конфликтовать с теми, что были добавлены в Perl. Для того, чтобы исправить этот конфликт в Perl убрали конфликтующие определения и попытались насколько возможно преобразовывать WSAE* коды в E* коды, чтобы избежать поломки обратной совместимости. Например, в соответствующих модулях Perl при присвоение значения $! код ошибки предварительно преобразуется из WSAE* в E*, но даже этого оказалось мало, поэтому в 5.19.4 был сделан хак, который при присвоении $! даже в коде пользователя автомагически преобразовывает WSAE* код в E* код, например:

C:\> perl -le "$! = 10061; print 0+$!"
107

Последнее изменение формально и сломало AnyEvent, который использовал статические значение константы WSAEWOULDBLOCK 10035, которое в новом Perl транслировалось в POSIX-значение EWOULDBLOCK(140):

} elsif ($! != EAGAIN && $! != EINTR && $! != WSAEWOULDBLOCK) {

Кстати, в IO::Socket появились конструкции, которые начинают дополнительно проверять версию Perl, прежде чем проверять код ошибки (в просторечии — подпорки к костылям):

elsif (!connect($sock,$addr) &&
        not ($!{EISCONN} || ($^O eq 'MSWin32' &&
        ($! == (($] < 5.019004) ? 10022 :
            Errno::EINVAL))))

К сожалению, проблема значительно более серьёзна, чем просто поломка обратной совместимости. Дело в том, что некоторые коды WSAE* не соответствуют POSIX-кодам E*. Например, ошибка WSAEINPROGRESS имеет абсолютно другой смысл, чем EINPROGRESS. Т.к. WSAEINPROGRESS возникает, когда процесс(поток) пытается выполнить операцию с сокетом, на котором уже проводится блокирующая операция, в то время как EINPROGRESS в POSIX означает, что сокет в неблокирущемся состоянии и соединение не может быть выполнено мгновенно без блокирования. Т.е. насильственное приведение WSAEINPROGRESS к EINPROGRESS ломает правильно работающую программу и вообще затрудняет написание переносимых программ:

# фрагмент tcp_connect() из AnyEvent::Socket

(connect $state{fh}, $sockaddr)
|| ($! == Errno::EINPROGRESS # POSIX
    || $! == Errno::EWOULDBLOCK
    # WSAEINPROGRESS intentionally not checked - it means something else entirely
    || $! == AnyEvent::Util::WSAEINVAL # not convinced, but doesn't hurt
    || $! == AnyEvent::Util::WSAEWOULDBLOCK)

Подобные же несоответствия есть у кодов WSAEMFILE, WSAEINTR, WSAEFAULT, WSAEACCESS, которые никак не соответствуют кодам из POSIX. Да и странно пытаться привести не POSIX-систему Windows в рамки POSIX-стандарта.

Perl в этом отношении не одинок, практически все языки (Ruby, Python, Tcl, PHP) точно также слепо копируют WSAE* коды в E* без оглядки на их реальное действие, что приводит к тому, что программы работающие под windows ведут себя неправильно и наполнены трудноуловимыми глюками.