пятница, 15 июля 2016 г.

C++: загадки инициализации глобальных объектов

Давайте напишем простую структуру, объекты которой смогут выводить на экран время старта программы. Файл pst.h
#include <ctime>

struct ProgramStartTime
{
    ProgramStartTime();

    char *  operator()( void ) const;
    time_t  now_;
};

extern ProgramStartTime  pst_ref;
Файл pst.cc
#include "pst.h"

namespace
{
    time_t  now( time( NULL ) );
}

ProgramStartTime::ProgramStartTime() : now_( now )
{
}

char *  ProgramStartTime::operator()( void ) const
{
    return ctime( &now_ );
}

ProgramStartTime  pst_ref;
Все просто. Время старта программы будем отсчитывать с момента динамической инициализации глобальной переменной now. Функция operator()( void ) структуры ProgramStartTime будет возвращать время now_, инициализированное значением now в конструкторе ProgramStartTime и преобразованное в удобоваримую форму с помощью функции ctime(). Глобальный объект pst_ref будет опорным объектом типа ProgramStartTime в дальнейшем исследовании. А теперь функция main() с собственным глобальным объектом pst. Файл test.cc
#include <iostream>
#include "pst.h"

ProgramStartTime  pst;

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << pst()
              << "Reference start time: " << pst_ref();
}
Компилируем и запускаем программу test.
g++ -Wall -o test pst.cc test.cc
./test
          Start time: Fri Jul 15 09:40:11 2016
Reference start time: Fri Jul 15 09:40:11 2016
Все хорошо, не так ли? Да ну! А как вам такое?
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Thu Jan  1 03:00:00 1970
Reference start time: Thu Jan  1 03:00:00 1970
Ух ты, семидесятый год, начало эпохи! Видимо глобальный объект now был инициализирован нулем! И что самое интересное, мы ведь не меняли исходный код программы, а просто изменили порядок перечисления исходных файлов в списке аргументов g++. Давайте копнем глубже, распечатаем значения членов now_ в объектах pst и pst_ref.
#include <iostream>
#include "pst.h"

ProgramStartTime  pst;

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << pst()
              << "Reference start time: " << pst_ref();
    std::cout.width( 28 );
    std::cout << "Plain start time: " << pst.now_ << std::endl
              << "Plain reference start time: " << pst_ref.now_ << std::endl;
}
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Thu Jan  1 03:00:00 1970
Reference start time: Thu Jan  1 03:00:00 1970
          Plain start time: 0
Plain reference start time: 1468565544
Ага, в объекте pst_ref хранится хорошее время, а в объекте pstноль, то есть начало эпохи. Отложим разбор этого странного факта на потом, пока нужно хотя бы понять, почему функция ctime() в обоих случаях возвращает начало эпохи. Справочное руководство по ctime() говорит, что эта функция возвращает результат в статическом буфере, поэтому она не является потокобезопасной. Нас в данном случае интересует другое. Возвращаемые значения, будучи помещенными несколько раз в один участок памяти в пределах одного выражения (sequence в смысле потока выполнения), могут оказаться одинаковыми: в конце концов это один и тот же участок памяти! Помните известное правило о непредсказуемости порядка вычисления функций в одном выражении? Так вот, в данном случае с функциями, возвращающими результат в глобальном объекте-указателе, это правило распространяется и на возвращаемый результат. Это и есть мораль номер 1. Функции, возвращающие результат вычисления в глобальных объектах, помимо общеизвестных недостатков, таких как небезопасность в многопоточной среде, обладают рядом менее известных проблем, таких как неопределенное поведение с точки зрения возвращаемого значения в пределах одного выражения. Давайте разобьем единое выражение с вызовами ctime() на два и продолжим выяснять, почему в объектах pst и pst_ref хранятся разные значения now_ и как это зависит от порядка перечисления исходных файлов в списке аргументов g++.
#include <iostream>
#include "pst.h"

ProgramStartTime  pst;

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << pst();
    std::cout << "Reference start time: " << pst_ref();
    std::cout.width( 28 );
    std::cout << "Plain start time: " << pst.now_ << std::endl
              << "Plain reference start time: " << pst_ref.now_ << std::endl;
}
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Thu Jan  1 03:00:00 1970
Reference start time: Fri Jul 15 10:29:32 2016
          Plain start time: 0
Plain reference start time: 1468567772
Теперь все хорошо с точки зрения соответствия между значениями now_ и вызовами ctime( &now_ ). Но что же не так с самим значением now_ в объекте pst? Обратим внимание на то, что глобальные объекты pst и now определены в разных файлах, то бишь единицах трансляции (translation unit). Вот вам и объяснение и мораль номер 2. Корректная динамическая инициализация зависимых глобальных объектов в разных единицах трансляции не гарантируется. В нашем примере объект pst в лице конструктора его типа зависит от глобального объекта now, динамически (то есть во время выполнения программы) инициализируемого значением, возвращаемым функцией time(). Заметим, что pst тоже инициализируется динамически, и в этом вся соль. Порядок такой инициализации между разными единицами трансляции не определен, несмотря на то, что один из объектов зависит от другого: C++ не отслеживает такого рода зависимости. Зато статическая инициализация глобальных объектов нулями (в том числе и тех, которые в дальнейшем будут инициализированы динамически) происходит раньше любой динамической, вот почему член now_ в объекте pst равен нулю, а не какому-нибудь другому произвольному значению. С другой стороны, объект pst_ref всегда инициализируется корректно. Но это и не удивительно: его определение находится после определения now в той же единице трансляции, а в этом случае стандарт гарантирует последовательную динамическую инициализацию глобальных объектов. В том, что изменение порядка перечисления исходных файлов в списке аргументов g++ повлияло на корректность программы, нет никакого правила: этот эффект нужно рассматривать как особенность конкретного линковщика на конкретной платформе. И, наконец, мораль номер 3 (позитивная). Чтобы избежать эффектов динамической инициализации глобальных объектов, пользуйтесь локальными статическими объектами, возвращаемыми из специально подготовленных для этого функций-оберток. Динамическая инициализация локальных статических объектов происходит после первого обращения к функции-обертке, поэтому, при условии, что эта функция-обертка в первый раз вызывается из функции main() или глубже по стеку, все глобальные объекты, от которых зависит возвращаемый статический объект, будут гарантированно инициализированы до его собственной инициализации. Условие динамической инициализации локальных статических объектов после первого вызова функции, в которой они определены, позволяет настроить цепочку инициализаций зависимых статических объектов (или так называемый dependency tracking) при условии, что все они размещены как локальные статические объекты в собственных функциях-обертках. Я покажу, как в нашем примере обернуть глобальный объект pst (я не стал оборачивать объект now ради формирования цепочки инициализаций, поскольку в этом случае время его инициализации будет совпадать со временем первого вызова функции-обертки и, в принципе, оно может сильно не совпадать со временем старта программы).
#include <iostream>
#include "pst.h"

ProgramStartTime *  getPst( void )
{
    static ProgramStartTime  pst;
    return &pst;
}

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << getPst()->operator()();
    std::cout << "Reference start time: " << pst_ref();
    std::cout.width( 28 );
    std::cout << "Plain start time: " << getPst()->now_ << std::endl
              << "Plain reference start time: " << pst_ref.now_ << std::endl;
}
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Fri Jul 15 11:41:47 2016
Reference start time: Fri Jul 15 11:41:47 2016
          Plain start time: 1468572107
Plain reference start time: 1468572107

среда, 6 июля 2016 г.

Conky: мои настройки отображения погоды (yahooapis и jq) и музыки (cmus)

Оригинальные настройки conky я взял отсюда. В этот пакет, кроме собственно конфигураций окон, входят шрифты для отображения простого текста и погодных символов. К сожалению, в мае этого года окно weather_date перестало показывать погоду. Как выяснилось, это окно использует yahooapis, а Yahoo изменил свой API как раз в это время. В общем, пришлось переписать это окно для новых yahooapis, а заодно улучшить производительность за счет уменьшения вызовов процессов-фильтров, познакомиться с замечательным парсером JSON jq и перевести настройки в новый формат с помощью скрипта convert.lua, который поставляется вместе с обновленным conky. На следующей картинке показано, как выглядит окно weather_date на моих рабочем и домашнем компьютерах.
Запускать conky с окном weather_date нужно так:
conky -c ~/.grayscale/conkyrc/weather_date
А это сам скрипт weather_date.
conky.config = {
--###############
--###############PERFORMANCE_SETTINGS
--###############
    update_interval = 5,
    total_run_times = 0,
    net_avg_samples = 2,
    imlib_cache_size = 0,
    double_buffer = true,
    no_buffers = true,

--###############
--###############TEXT_SETTINGS
--###############
    use_xft = true,
    font = 'GE Inspira:bold:pixelsize=12',
    xftalpha = 0.1,
    override_utf8_locale = true,
    text_buffer_size = 512,

--###############
--###############WINDOW_SPECIFICATIONS
--###############
    background = true,
    own_window = true,
    own_window_transparent = true,
    own_window_type = 'normal',
    own_window_class = 'conky-semi',
    own_window_hints = 'undecorated,below,sticky,skip_taskbar,skip_pager',
    own_window_argb_visual = true,
    own_window_argb_value = 0,
    draw_outline = false,
--# Window border
    draw_borders = false,
    pad_percents = 0,
    border_inner_margin = 4,
    top_name_width = 10,
    use_spacer = 'right',
--#Size and position
    alignment = 'top_left',
    gap_x = 1600,
    gap_y = 58,
    minimum_width = 0, minimum_height = 0,
    maximum_width = 240,

--###############
--###############GRAPHICS_SETTINGS
--###############
    draw_shades = false,
    default_shade_color = '#292421',
    short_units = true,
--#Default Colors
    default_color = '#efefef',
    default_shade_color = '#1d1d1d',
--#Color Title
    color1 = '#bcbcbc',
    color2 = '#00d787',
    color3 = '#00d787',
};

conky.text = [[
#################
#################DATE & TIME
#################
${voffset 10}${font GE Inspira:pixelsize=50}${color1}${time %H:%M}\
${voffset -20}${offset 5}${font GE Inspira:pixelsize=25}${color2}${time %d}\
${voffset -15}${font GE Inspira:pixelsize=20}${color1}${time  %b}${time %Y}\
${voffset 35}${offset -105}${font GE Inspira:pixelsize=22}${color1}${time %A}\
${color}${font}
${voffset 5}${color3}${hr 2}${color}\

################
################DOWNLOADING WEATHER INFO AND SAVING IT AS ~/.cache/weather.json
################
${execi 1800 nm-online -t 60 && curl -s \
    -G 'http://query.yahooapis.com/v1/public/yql?format=json' \
    --data-urlencode 'q=select * from weather.forecast where woeid in \
    (select woeid from geo.places(1) where text="Saint-Petersburg, Russia") \
    and u="c"' -o ~/.cache/weather.json}
################MAIN WEATHER IMAGE
${voffset -10}${offset 20}${font conkyweather:size=140}${color1}\
${execi 1800 grep "^$(jq -r '.query.results.channel.item.condition.code' \
    ~/.cache/weather.json) =" ~/.grayscale/data/compare | cut -d " " -f3}\
${color}${font}
################WEATHER CONDITIONS
${alignc 10}${font GE Inspira:bold:pixelsize=15}${color2}\
${execi 1800 jq -r '.query.results.channel.item.condition.text' \
    ~/.cache/weather.json}${color}${font}
##############EXTRACTING CURRENT/HIGH TEMP IN DEGREE CELSIUS
${offset 30}${font GE Inspira:pixelsize=50}${color3}\
${execi 1800 jq -r '.query.results.channel.item.condition.temp' \
    ~/.cache/weather.json}°C/${font GE Inspira:pixelsize=30}\
${color3}${execi 1800 jq -r '.query.results.channel.item.forecast[0].high' \
    ~/.cache/weather.json}°C\
${color}${font}\

#################
#################EXTRACTING LOCATION
#################
${voffset 16}${offset 16}${font GE Inspira:bold:pixelsize=20}${color1}\
${execi 1800 jq -j '.query.results.channel.location | .city + ", ", .country' \
    ~/.cache/weather.json}${color}${font}
${color3}${hr 2}${color}\

#################
#################EXTRACTING WEATHER INFO
#################
##PRESSURE     HUMIDITY
${voffset 5}${font GE Inspira:bold:pixelsize=12}${color2}\
Pressure : ${color1}\
${execi 1800 jq -r '.query.results.channel.atmosphere.pressure' \
    ~/.cache/weather.json}mb\
${alignr -16}${font GE Inspira:bold:pixelsize=12}${color2}\
Humidity : ${color1}\
${execi 1800 jq -r '.query.results.channel.atmosphere.humidity' \
    ~/.cache/weather.json}%\
${color}${font}
##SUNRISE     SUNSET
${font GE Inspira:bold:pixelsize=12}${color2}\
Sunrise : ${color1}\
${execi 1800 jq -r '.query.results.channel.astronomy.sunrise' \
    ~/.cache/weather.json}\
${alignr -16}${font GE Inspira:bold:pixelsize=12}${color2}\
Sunset : ${color1}\
${execi 1800 jq -r '.query.results.channel.astronomy.sunset' \
    ~/.cache/weather.json}${color}${font}
##WIND     VISIBILITY
${font GE Inspira:bold:pixelsize=12}${color2}\
Wind : ${color1}${execi 1800 jq -r '.query.results.channel.wind.speed' \
    ~/.cache/weather.json}km/hr\
${alignr -16}${font GE Inspira:bold:pixelsize=12}${color2}\
Visibility : ${color1}\
${execi 1800 jq -r '.query.results.channel.atmosphere.visibility' \
    ~/.cache/weather.json}km${color}${font}
${color3}${hr 2}${color}\

#################
#################WEATHER FORECAST IMAGES FOR NEXT 2 DAYS
#################
${voffset 10}${font conkyweather:size=70}${color1}\
${execi 1800 grep "^$(jq -r '.query.results.channel.item.forecast[1].code' \
    ~/.cache/weather.json) =" ~/.grayscale/data/compare | cut -d " " -f3}\
${tab 72}${font conkyweather:size=70}${color1}\
${execi 1800 grep "^$(jq -r '.query.results.channel.item.forecast[2].code' \
    ~/.cache/weather.json) =" ~/.grayscale/data/compare | cut -d " " -f3}\
${color}${font}\

#################
#################EXTRACTING LOW/HIGH TEMP IN DEGREE CELSIUS FOR NEXT 2 DAYS
#################
${font GE Inspira:bold:pixelsize=15}${color2}\
${execi 1800 jq -r '.query.results.channel.item.forecast[1].day' \
    ~/.cache/weather.json} : ${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[1].low' \
    ~/.cache/weather.json}°/${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[1].high' \
    ~/.cache/weather.json}°\
${alignr -16}${font GE Inspira:bold:pixelsize=15}${color2}\
${execi 1800 jq -r '.query.results.channel.item.forecast[2].day' \
    ~/.cache/weather.json} : ${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[2].low' \
    ~/.cache/weather.json}°/${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[2].high' \
    ~/.cache/weather.json}°${color}${font}
${voffset 5}${color3}${hr 2}${color}\

#################
#################CALENDER DISPLAY
#################
${voffset 10}${font nimbus mono L:bold:size=12}${color1}\
${execpi 1800 DJS=`date +%_d`; cal | \
    sed -e s/"\(^\|[^0-9]\)$DJS"'\b'/'\1${color2}'"$DJS"'$color'/ -e s/^/'  '/}
]];
Внутри conky.config находятся настройки геометрии окна, объявляются различные цвета и т.п. Собственно содержимое окна описано внутри conky.text. В параграфе, озаглавленном как DOWNLOADING WEATHER …, каждые полчаса (1800 секунд) с помощью execi и curl, после предварительной проверки доступности сети командой nm-online, выполняется запрос на query.yahooapis.com в формате YQL (Yahoo! Query Language). Ответ от Yahoo сохраняется в файле ~/.cache/weather.json. Обратите внимание на то, что данная команда execi ничего не выводит на экран, то есть фактически в данном случае conky работает как cron! Я выбрал формат вывода JSON потому, что он легко и непринужденно парсится прямо из командной строки командой jq, которая позволяет задавать довольно сложные фильтры для поиска данных, предоставляя своеобразный язык запросов, который очень подробно, с примерами, описан в man jq. Все оставшиеся параграфы внутри conky.text вплоть до вывода календаря вызывают jq для поиска значений определенных погодных категорий внутри файла ~/.cache/weather.json. Например, внутри параграфа EXTRACTING LOCATION выполняется запрос jq -j '.query.results.channel.location | .city + ", ", .country' ~/.cache/weather.json, который выводит значения city и country, находящиеся в пути /query/results/channel/location структуры JSON и выводит их в одной строке (опция -j). Поскольку jq работает из командной строки, имеется возможность выводить погодные условия прямо на терминал! Например, погоду на завтра можно вывести командой
jq '.query.results.channel.item.forecast[1]' ~/.cache/weather.json
{
  "code": "12",
  "date": "07 Jul 2016",
  "day": "Thu",
  "high": "17",
  "low": "12",
  "text": "Rain"
}
При этом вывод в терминале будет синтаксически подсвечен! А теперь о выводе информации из cmus. Cmus — это аудиоплеер для терминала. В оригинальном пакете была представлена поддержка плеера clementine, но я им не пользуюсь. Информация из clementine выводилась в окне net_hdd с помощью execpi 2 ~/.grayscale/data/clementine, скрипт clementine в директории ~/.grayscale/data/ был написан на perl. Сначала я написал аналогичный скрипт cmus, тоже на perl, но в новом conky большое количество проблем заставило переписать его на lua и загружать в conky командой lua_load. Вот так выглядит информация из cmus, когда он запущен.
Для того, чтобы это заработало, в net_hdd, предварительно обработанном скриптом convert.lua, внутрь conky.config нужно добавить строку
        lua_load = '~/.grayscale/data/cmus.lua',
Внутрь conky.text, вместо настроек для clementine, нужно добавить строки
################
################CMUS_DISPLAY
################
${voffset 5}${offset 22}\
${font GE Inspira:bold:pixelsize=15}${color2}CMUS${voffset 2}\
${offset 5}${color3}${hr 2}${color}${font}
${if_running cmus}${lua_parse cmus}${else}${voffset 46}${endif}
Обратите внимание, что в случае, когда cmus не запущен, в окно net_hdd выводится пустой вертикальный сдвиг высотой 46 пикселей: это сделано для того, чтобы окно не мерцало при запуске и закрытии cmus. Величина сдвига была определена экспериментально: она должна точно соответствовать высоте совокупной информации из cmus, когда он запущен, выводимой conky. Кроме того, стоит отметить, что lua_parse, в отличие от execpi, не имеет собственной настройки интервала обновления данных, а следовательно скорость обновления данных определяется системной настройкой окна update_interval внутри conky.config. Скрипт cmus.lua в директории ~/.grayscale/data/ выглядит так.
function conky_echo(a)
    return a
end

function conky_cmus()
    local artist   = 'N/A';
    local title    = 'N/A';
    local album    = 'N/A';
    local progress = 0;
    local pos      = 0;
    local length   = 0;
    local status   = '';
    
    local color1   = 'bcbcbc';
    local color2   = 'ffa300';
    local color3   = 'ffff5f';
    
    f = assert( io.popen( 'cmus-remote -Q' ) ) or os.exit( 1 )
     
    for line in f:lines() do
        local v = string.match( line, '^status%s*(.*)' )
        if v ~= nil and v ~= '' and v ~= 'playing' then
            status = ' [' .. v .. ']'; goto next
        end
        v = string.match( line, '^duration%s*(.*)' )
        if v ~= nil and v ~= '' then length = tonumber( v ); goto next end
        v = string.match( line, '^position%s*(.*)' )
        if v ~= nil and v ~= '' then pos = tonumber( v ); goto next end
        v = string.match( line, '^tag album%s*(.*)' )
        if v ~= nil and v ~= '' then album = v; goto next end
        v = string.match( line, '^tag artist%s*(.*)' )
        if v ~= nil and v ~= '' then artist = v; goto next end
        v = string.match( line, '^tag title%s*(.*)' )
        if v ~= nil and v ~= '' then title = v; goto next end
        ::next::
    end
      
    f:close()
    
    if pos > 0 and length > 0 then
        progress = math.floor( 100 * pos / length )
    end
    
    return '${voffset 5}${offset 6}${font StyleBats:size=10}${color ' ..
            color1 .. '}k${voffset -2}${offset 3}${color ' .. color2 ..
            '}${font}Title: ${color ' .. color3 .. '}${alignr}' .. title ..
            '${color ' .. color2 .. '}' .. status ..
            '\n${offset 6}${font StyleBats:size=10}${color ' .. color1 ..
            '}k${voffset -2}${offset 3}${color ' .. color2 ..
            '}${font}Artist: ${color ' .. color3 .. '}${alignr}' .. artist ..
            '\n${offset 6}${font StyleBats:size=10}${color ' .. color1 ..
            '}k${voffset -2}${offset 3}${color ' .. color2 ..
            '}${font}Album: ${color ' .. color3 .. '}${alignr}' .. album ..
            '${color ' .. color1 .. '}${font}' ..
            '\n${voffset 1}${offset 8}${lua_bar echo ' .. progress .. '}'
end
Прошу меня простить, если что-то здесь сделано неизящно или неэффективно — это моя первая программа на lua :)

пятница, 1 июля 2016 г.

Проприетарные видеодрайверы Nvidia в репозиториях Fedora от negativo17

Очередной апгрейд Fedora (до версии 24) в очередной раз навернул графику на моем стареньком десктопе с Nvidia GeForce 8. На этот раз проблема оказалась в отсутствии нужных драйверов на RPM Fusion (их там нет до сих пор). Моей старой видеокарте нужен специальный драйвер серии 340xx. Поиск этой серии на Russian Fedora тоже не увенчался успехом. Перспектива собирать драйвер самому мне не улыбалась: нет никакого удовольствия делать это каждый раз при очередном обновлении ядра. В общем, лень — двигатель прогресса — привела меня на замечательный ресурс negativo17.org, в котором на странице repositories находится список репозиториев Fedora с разными несвободными приложениями типа Skype и Rar, в том числе и с проприетарными драйверами Nvidia. Инструкции по установке драйверов и прочие подробности изложены на этой странице. Поскольку мне был нужен драйвер серии 340xx, для установки репозитория я использовал команду
dnf config-manager --add-repo=http://negativo17.org/repos/fedora-nvidia-340.repo
Остальное всё по инструкции (почти).
dnf remove \*nvidia\*
dnf install nvidia-driver akmod-nvidia
akmods --force
И перезагрузка. Важно отметить, что автор репозиториев является активным мейнтейнером Fedora (см. здесь), поэтому их использование не должно создавать риск нарушения безопасности.

воскресенье, 31 января 2016 г.

vim-xkbswitch: управление keymap в нормальном режиме и в строках поиска

На днях добавил указанную фичу в vim-xkbswitch. Суть проста. При выходе из режима ввода (Insert mode) в нормальный режим (Normal mode) хотелось бы, чтобы после ввода команд r и f, а также в режиме поиска в командной строке (команды / и ?), системная раскладка переключалась на ту, что была в режиме ввода. К сожалению, реализовать такое с нуля технически весьма затруднительно. К счастью, можно воспользоваться встроенной в vim поддержкой keymap, и более того, управлять ею с помощью настройки iminsert и imsearch при выходе в нормальный режим. В общем, если есть желание управлять keymap из vim-xkbswitch, то в файл .vimrc следует добавить такие строки.
let g:XkbSwitchAssistNKeymap = 1    " for commands r and f
let g:XkbSwitchAssistSKeymap = 1    " for search lines
set keymap=russian-jcukenwin
set iminsert=0
set imsearch=0
Значение keymap в данном случае зависит от дополнительной раскладки, с которой вы работаете. Если по какой-то причине имя системной раскладки (возвращается бэкенд-библиотекой, например xkb-switch) не совпадает с именем keymap (которое обычно присваивается переменной b:keymap_name в файле keymap), то можно настроить отображение из первого во второе, например
let g:XkbSwitchKeymapNames = {'ru' : 'ru_keymap'}
Здесь ru — имя системной раскладки, а ru_keymap — предполагаемое значение b:keymap_name. Если вы работаете с несколькими раскладками одновременно (например, стандартной для нормального режима американской, русской и украинской), то возможно при выходе из режима ввода вы захотите, чтобы keymap указывал на ту из трех раскладок, которая была использована последней. Для этого вам понадобится динамическая настройка keymap, которую можно задействовать с помощью следующих настроек в .vimrc (вместо тех, что были приведены выше).
let g:XkbSwitchAssistNKeymap = 1    " for commands r and f
let g:XkbSwitchAssistSKeymap = 1    " for search lines
let g:XkbSwitchDynamicKeymap = 1
let g:XkbSwitchKeymapNames = {'ru' : 'russian-jcukenwin',
            \ 'uk' : 'ukrainian-jcuken'}
Обратите внимание, здесь не требуется настройка keymap по умолчанию (хотя и не возбраняется), а в качестве значений в словаре g:XkbSwitchKeymapNames используются имена keymap, а не значения b:keymap_name. Если в нормальном режиме вы захотите переключиться на стандартную раскладку нормального режима, то просто временно зайдите в режим ввода и в нем переключитесь на соответствующую раскладку (это можно также сделать командой :setlocal iminsert=0). В строке поиска то же самое можно сделать сочетанием клавиш Ctrl-^. Все это хорошо, но есть одна серьезная проблема. Системный индикатор раскладки будет по-прежнему показывать стандартную в нормальном режиме раскладку, ведь vim-xkbswitch здесь не действует, а keymap не влияет на системную раскладку. На помощь приходит статусная строка, которую можно легко настроить для отображения значения keymap, когда iminsert равен 1, в плагине Powerline. Индикатор keymap будет отображать активную keymap раскладку для команд r и f и строк поиска, в случае, если iminsert равен 1, в то время как индикатор системной раскладки будет отображать стандартную для нормального режима раскладку. В режиме ввода индикатор keymap не действует, поскольку vim-xkbswitch отключает keymap установкой iminsert в 0. Такое решение, кстати, позволяет пользователю видеть, какая раскладка станет активной при переходе из нормального режима в режим ввода, а это здо́рово, на мой взгляд! Я покажу, как настроить индикатор keymap в старом Powerline, в новейших репликах типа vim-airline действия должны быть похожи. Итак, в файле autoload/Powerline/Segments.vim в массиве, передаваемом в функцию Pl#Segment#Init() перед строкой, содержащей слово fileformat, вставляем строки
    \ Pl#Segment#Create('keymap_name'     ,
        \'%{&iminsert && exists("b:keymap_name") ? b:keymap_name : ""}',
        \ Pl#Segment#Modes('!N')),
, в файле autoload/Powerline/Themes/default.vim (или в другом файле темы Powerline, в зависимости от того, какую тему вы используете) в список, передаваемый в функцию Pl#Theme#Buffer(), перед строкой, содержащей слово fileformat, вставляем строку
        \ , 'keymap_name'
, и, наконец, в файле autoload/Powerline/Colorschemes/default.vim (или в другом файле цветовой схемы Powerline, если вы используете нестандартную схему), в произвольном месте списка, передаваемого в функцию Pl#Colorscheme#Init(), вставляем строки
    \ Pl#Hi#Segments(['keymap_name'], {
        \ 'n': ['brightestorange', 'gray2'],
        \ 'i': ['brightestorange', 'darkestblue'],
        \ }),
    \
После этого выполняем команду
:PowerlineClearCache
, перезагружаем vim и наслаждаемся индикацией keymap. Напоминаю, что стандартная в нормальном режиме раскладка отображаться не будет. Вот так может выглядеть статусная строка Powerline с индикатором keymap (обратите внимание на оранжевые буквы ru).

воскресенье, 27 декабря 2015 г.

nginx module to enable haskell binding to nginx configuration files

Do you like haskell and nginx? I love them both and this inspired me to write an nginx module for inlining haskell source code straight into nginx configuration files. The module was published on github as nginx-haskell-module and shipped with an nginx configuration example to show its basic usage. Let’s look at it.
user                    nobody;
worker_processes        2;

events {
    worker_connections  1024;
}

http {
    default_type        application/octet-stream;
    sendfile            on;

    haskell ghc_extra_flags '-hide-package regex-pcre';

    haskell compile /tmp/ngx_haskell.hs '

import qualified Data.Char as C
import           Text.Regex.PCRE
import           Safe

toUpper = map C.toUpper
NGX_EXPORT_S_S (toUpper)

takeN x y = take (readDef 0 y) x
NGX_EXPORT_S_SS (takeN)

NGX_EXPORT_S_S (reverse)

matches :: String -> String -> Bool
matches a b = not $ null (getAllTextMatches $ a =~ b :: [String])
NGX_EXPORT_B_SS (matches)

    ';

    server {
        listen       8010;
        server_name  main;
        error_log    /tmp/nginx-test-haskell-error.log;
        access_log   /tmp/nginx-test-haskell-access.log;

        location / {
            haskell_run toUpper $hs_a $arg_a;
            echo "toUpper ($arg_a) = $hs_a";
            if ($arg_b) {
                haskell_run takeN $hs_a $arg_a $arg_b;
                echo "takeN ($arg_a, $arg_b) = $hs_a";
                break;
            }
            if ($arg_c) {
                haskell_run reverse $hs_a $arg_c;
                echo "reverse ($arg_c) = $hs_a";
                break;
            }
            if ($arg_d) {
                haskell_run matches $hs_a $arg_d $arg_a;
                echo "matches ($arg_d, $arg_a) = $hs_a";
                break;
            }
        }
    }
}
Haskell source code is placed inside the second argument of the directive haskell compile. In this example it contains some imports, three definitions of functions and four special export directives to introduce the functions on the nginx configuration level. There are four types of export directives: NGX_EXPORT_S_S, NGX_EXPORT_S_SS, NGX_EXPORT_B_S and NGX_EXPORT_B_SS for functions of types String -> String, String -> String -> String, String -> Bool and String -> String -> Bool respectively. The code gets written into the path specified in the first argument of the directive haskell compile (it must be an absolute path) and then compiled to a shared library at the very start of nginx. Sometimes ghc may require extra options besides defaults. And here is the case. As soon as import of Text.Regex.PCRE can be ambiguous (because two haskell packages regex-pcre and regex-pcre-builtin provide it), ghc must know which package to use. There is a special ghc flag -hide-package for hiding unwanted packages and it was used in this example by the directive haskell ghc_extra_flags. There is another nginx haskell directive haskell load which is similar to the haskell compile except it does not require the second argument (i.e. the haskell source code). The directive tries to load compiled shared library that corresponds to the path specified in its first argument (/tmp/ngx_haskell.so in this example). If the code argument is present but there is not compiled shared library, the latter will be first compiled and then loaded. If the haskell code fails to compile then nginx won’t start (or won’t reload workers if the code had been wrongly changed in the configuration file and nginx has been sent the SIGHUP signal). Any errors will be logged. To run the compiled haskell code in the nginx context there is another nginx directive haskell_run. It is allowed in server, location and location-if configuration clauses and may accept three or four arguments depending on the arity of the exported function to run which is specified in the first argument of the directive. The second argument introduces an nginx variable where return value of the haskell function will be saved. For example directive
                haskell_run takeN $hs_a $arg_a $arg_b;
introduces a new nginx variable $hs_a which will be calculated on demand as result of running an exported haskell function takeN with arguments $arg_a and $arg_b. Let’s do some curl tests. First of all nginx must be built and run. Besides the haskell module the build requires nginx echo module. It must be specified in options --add-module of nginx configure script.
./configure --add-module=<path-to-nginx-echo-module> --add-module=<path-to-nginx-haskell-module>
Placeholders <path-to-nginx-... are to be replaced with real paths to the modules. After running make we start the nginx daemon.
./objs/nginx -c /home/lyokha/devel/nginx-haskell-module/nginx.conf
[1 of 1] Compiling NgxHaskellUserRuntime ( /tmp/ngx_haskell.hs, /tmp/ngx_haskell.o )
Linking /tmp/ngx_haskell.so ...
Nginx option -c specifies location of the configuration file. The shared library /tmp/ngx_haskell.so was built upon start which was printed on the terminal. And now we are going to ask nginx server to do some haskell calculations!
curl 'http://localhost:8010/?a=hello_world'
toUpper (hello_world) = HELLO_WORLD
curl 'http://localhost:8010/?a=hello_world&b=4'
takeN (hello_world, 4) = hell
curl 'http://localhost:8010/?a=hello_world&b=oops'
takeN (hello_world, oops) = 
curl 'http://localhost:8010/?c=intelligence'
reverse (intelligence) = ecnegilletni
curl 'http://localhost:8010/?d=intelligence&a=^i'
matches (intelligence, ^i) = 1
curl 'http://localhost:8010/?d=intelligence&a=^I'
matches (intelligence, ^I) = 0
Hmm, I did not escape caret inside the URL in the matches examples: seems that curl allowed it, anyway the calculations were correct. Let’s do some changes in the haskell code inside the configuration file, say takeN will be
takeN x y = ("Incredible! " ++) $ take (readDef 0 y) x
, reload nginx configuration
pkill -HUP nginx
and do curl again.
curl 'http://localhost:8010/?a=hello_world&b=4'
takeN (hello_world, 4) = Incredible! hell
Nice, kind of runtime haskell reload! Now I want to explain some details of implementation and concerns about efficiency and exceptions. Haskell code gets compiled while reading nginx configuration file. It means that ghc must be accessible during nginx runtime (except when using haskell load with already compiled haskell library). Only one haskell compile (or haskell load) directive is allowed through the whole configuration. Directive haskell compile (or haskell load) must be placed before directives haskell_run and after directive haskell ghc_extra_flags (if any). Compiled haskell library gets loaded by dlopen() call when starting a worker process (i.e. haskell runtime is loaded for each nginx worker process separately). The haskell code itself is wrapped inside a special haskell FFI code (it can be found in /tmp/ngx_haskell.hs in case of the above example). The FFI tries its best to minimize unnecessary allocations when reading arguments that were passed from nginx, however for S_S and S_SS function types it does allocate a new string for return value which gets promptly copied to an nginx memory pool and freed. It was possible to avoid allocations for return values, but in this case ghc should have known about internal nginx string implementation, so I decided to sacrifice efficiency to avoid runtime dependency on nginx source code (again, this dependency would not have been necessary if this special FFI interface had been compiled in a separate module, but… probably I’ll do that in future). Another concern about efficiency is using in the exported haskell handlers haskell Strings which are simple lists of characters (which is assumed to be inefficient). On the other hand they are lazy (and this often means efficient). Ok, this is a traditional haskell trade-off matter… What about haskell exceptions? C code is unable to handle them and this is a bad news. However writing pure haskell code makes a strong guarantee about high level of exception safety (at least all IO exceptions must go). In the above example I used function readDef from module Safe instead of its partial counterpart read which increased safety level as well.