четверг, 17 марта 2016 г.

[prog.flame] Программисты-сишники явно намного более трудолюбивы, чем я...

Ну вот взять ув.тов.asfikon-а (он же Eax Melanhovich) и его свежий пост про работу с libcurl из C (вот целиком исходный файл, о котором пойдет речь). Хватило же у человека терпения и прилежания тщательно выписывать очистку ресурсов при обнаружении ошибок в работе с libcurl:

CURLcode res = curl_easy_perform(curl);
if(res != CURLE_OK)
{
    fprintf(stderr"curl_easy_perform() failed: %s\n",
      curl_easy_strerror(res));
    writeFuncCtxFree(&ctx);
    curl_formfree(formFirstItem);
    curl_slist_free_all(curlHeaders);
    curl_easy_cleanup(curl);
    return 1;
}

Но вообще очевидно, что программирование в таком стиле -- это занятие настолько муторное, что нет-нет, да и забудешь где-нибудь код возврата проверить:

curl_easy_setopt(curl, CURLOPT_URL, "http://imageshack.us/upload_api.php");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curlHeaders);
curl_easy_setopt(curl, CURLOPT_HTTPPOST, formFirstItem);

Поначалу может казаться, что тут все тривиально и ошибкам возникать неоткуда... Но откуда только что берется со временем ;)

Так вот, возвращаясь к основной мысли: в последние лет 5 регулярно замечаю, что многие люди, которые явно умнее меня, пишут код вот в таком вот простом стиле. Без изысков. Без попыток упростить себе жизнь. Ув.тов.asfikon просто хороший пример этого явления -- у него есть блог, где он публикует фрагменты своего кода, поэтому приводить примеры оттуда проще.

И не очень понятно почему так.

Сам я очень давно убедился в том, что не смогу нормально писать в таком стиле. Годах в 1991-1992-ом, когда более-менее серьезно попрограммировал на Pascal и C. Не хватает у меня терпения и внимательности для того, чтобы следить за тем количеством деталей, за которым приходится следить в C. Потому и ушел на C++ как только разобрался, что к чему.

А пример работы с libcurl хорош тем, что он иллюстрирует количество вещей, за которыми нужно следить при работе с C-шным API libcurl-а. Поэтому лет шесть назад, когда довелось втягивать libcurl в тогдашние проекты, дабы не заниматься всей этой низкоуровневой кухней, была написана простенькая C++ная библиотека, практически header-only. Под катом код примера из состава этой библиотеки.

Этот пример был написан где-то лет 5 или 6 назад, еще для C++03. Он демонстрирует основные вещи, связанные с libcurl и его curl_easy-интерфейсом: инициализация libcurl, создание и уничтожение объекта CURL, использование libcurl для escaping-а и unescaping-а строк, установка опций, установка callback-ов для получения HTTP-заголовков от сервера, установка debug-callback-а для трассировки выполняемых libcurl действий. Ну и, конечно же, выполнение HTTP GET и HTTP POST.

Коды ошибок для всех действий, связанных с libcurl контролируются автоматически. В случае возникновения проблем порождаются исключения. Дабы диагностика была более информативной, часть действий выполняется посредством макроса CURL_EASY_SAFE_SETOPT, который нужен для того, чтобы в исключение попадало не только описание ошибки от самой libcurl, но и название опции, при установке которой возникла ошибка. Так, если ошибка возникает в конструкции CURL_EASY_SAFE_SETOPT( handle, CURLOPT_POST, 1 ), то в описании ошибки будет присутствовать название "CURLOPT_POST".

Ну а вот, собственно, и код. Мне интересно, действительно ли такой C++ сложнее для понимания, чем чистый C? Или же, глядя на него "хочется обнять и плакать (с)"? ;)

#include <iostream>
#include <stdexcept>
#include <sstream>

#include <libcurl_tools_1/h/easy.hpp>

using namespace libcurl_tools_1;

class cerr_debug_functor_t
   :  public easy_debug_functor_t
   {
   public :
      virtual int
      handle( curl_infotype type, cpp_util_2::string_piece_t what )
         {
            switch( type )
               {
               case CURLINFO_TEXT : std::cerr << "# "break;
               case CURLINFO_HEADER_IN : std::cerr << "> "break;
               case CURLINFO_HEADER_OUT : std::cerr << "< "break;
               };

            std::cerr.write( what.data(), what.size() );
            
            return 0;
         }
   };

class cout_header_functor_t
   :  public easy_header_functor_t
   {
   public :
      virtual size_t
      handle( cpp_util_2::string_piece_t what )
         {
            std::cout << "HTTP Header: '" << what.to_string() << "'" << std::endl;
            return what.size();
         }
   };

void
do_escape_unescape_test( const easy_handle_t & handle )
   {
      const std::string source_str =
            "This is a source string + some symbols: =!@#$%^&*()";

      easy_url_escaped_string_t escaped( handle, source_str );
      easy_url_unescaped_string_t unescaped( handle, escaped.get() );

      std::cout << "URL escape/unescape test:\n"
            "\tsource:    " << source_str
            << "\n\tescaped:   " << escaped.get()
            << "\n\tunescaped: " << unescaped.get().to_string()
            << std::endl;
   }

void
do_test_get( const easy_handle_t & handle )
   {
      easy_auto_reseter_t option_reseter( handle );

      cerr_debug_functor_t debug_functor;
      easy_setup_debug_functor( handle, debug_functor );

      cout_header_functor_t header_functor;
      easy_setup_header_functor( handle, header_functor );

      std::stringstream response_stream;

      easy_setup_std_ostream_as_writedata( handle, response_stream );

      const char * url = "http://www.ya.ru";
      CURL_EASY_SAFE_SETOPT( handle, CURLOPT_URL, url );

      easy_perform_with_error_buffer( handle );

      std::cout << "RESULT is: " << std::endl << response_stream.str() << std::endl;
   }

void
do_test_post( const easy_handle_t & handle )
   {
      easy_auto_reseter_t option_reseter( handle );

      cerr_debug_functor_t debug_functor;
      easy_setup_debug_functor( handle, debug_functor );

      std::stringstream output_stream(
            std::ios_base::in | std::ios_base::out | std::ios_base::binary );
      std::stringstream response_stream;

      output_stream <<
         "<message bearer=\"HTTP\" uid=\"CENSORED\">\n"
         "\t<sn>42</sn>\n"
         "\t<payload>CENSORED</payload>\n"
         "</message>\n";

      rewindable_istream_as_readdata_t< std::stringstream > readdata(
            entire_istream( output_stream ) );
      readdata.setup_to_curl_handle( handle );

      easy_setup_std_ostream_as_writedata( handle, response_stream );

      const char * url = "http://localhost:8080/simple_echo.rb";
      CURL_EASY_SAFE_SETOPT( handle, CURLOPT_URL, url );

      slist_wrapper_t headers;
      headers.append( "Content-Type: text/xml" ).append( "Connection: Keep-Alive" );
      CURL_EASY_SAFE_SETOPT( handle, CURLOPT_HTTPHEADER, headers.head() );

      CURL_EASY_SAFE_SETOPT( handle, CURLOPT_POST, 1 );
      CURL_EASY_SAFE_SETOPT(
            handle, CURLOPT_POSTFIELDSIZE,
                  static_cast< curl_off_t >( output_stream.str().size() ) );

      easy_perform_with_error_buffer( handle );

      std::cout << "RESULT is: " << std::endl << response_stream.str() << std::endl;
   }

int
main( int argc, char ** argv )
   {
      try
         {
            global_initializer_t curl_initializer;
            easy_handle_t handle;

            do_escape_unescape_test( handle );
            do_test_get( handle );
            do_test_post( handle );
            do_test_post( handle );
         }
      catchconst std::exception & x )
         {
            std::cerr << "*** exception: " << x.what() << std::endl;
         }
   }

Еще позволю себе несколько пояснений по коду.

В main-е не зря делается два вызова do_test_post(), это проверка для работы такой штуки, как easy_auto_reseter_t. Можно заметить, что в начале do_test_get и do_test_post объявляется переменная типа easy_auto_reseter_t. Деструктор этой переменной автоматически вызывает curl_easy_reset(), что сбрасывает все изменения, внесенные в конкретный экземпляр CURL внутри функции. Бывает очень полезно, когда один и тот же CURL-объект используется в разных операциях.

При осуществлении HTTP POST используется rewindable_istream_as_readdata_t. Насколько я помню, этот класс появился как следствие обнаружения вот этой проблемы при использовании istream-ов совместно с Keep-Alive. Данный класс позволяет автоматически вернутся к началу отсылаемого на сервер фрагмента, если был обнаружен разрыв старого соединения и создание нового.

Для выполнения GET и POST запросов используется функция easy_perform_with_error_buffer. Эта функция устанавливает CURLOPT_ERRORBUFFER до вызова curl_easy_perform, затем сбрасывает эту опцию. Если же curl_easy_perform завершается неудачно, то при выбрасывании исключения используется содержимое error_buffer-а (там, как правило, оказывается более-менее вменяемое описание причины проблемы от самого libcurl).

Вообще, от работы с libcurl остались самые хорошие воспоминания (но из-за того, что работа с libcurl велась через собственную обертку). Жаль, что libcurl/curl не сделали их автора обеспеченным человеком, что, в прочем, обычное дело для реально полезных вещей. Главная претензия, которая у меня была в свое время к libcurl -- это использование autoconf. Из-за чего под Windows каждую версию библиотеки приходилось дорабатывать напильником (может сейчас что-то поменялось, не знаю).

Вроде как в свое время были какие-то плюсовые обертки над libcurl, но мне что-то они не понравились, проще оказалось сделать свой лисапет :)

Комментариев нет: