понедельник, 9 января 2017 г.

Памятка по компиляции shared libraries

Для начала код разделяемых библиотек (три файла: foo.h, foo1.cpp, foo2.cpp):

// foo.h
 
#ifndef foo_h__
#define foo_h__
 
extern "C"  
__attribute__ ((visibility("default"))) 
void foo(void);
 
#endif  // foo_h__
 
// foo1.cpp

#include <stdio.h>
#include "foo.h"
__attribute__((constructor)) void foo_init() { puts("Init shared library 1"); } __attribute__((destructor)) void foo_deinit() { puts("Deinit shared library 1"); } void foo(void) { puts("Hello, I'm a shared library 1"); }
// foo2.cpp

#include <stdio.h>
#include "foo.h"
__attribute__((constructor))
void foo_init()
{
    puts("Init shared library 2");
}

__attribute__((destructor))
void foo_deinit()
{
    puts("Deinit shared library 2");
}

void foo(void)
{
    puts("Hello, I'm a shared library 2");
}


Теперь код, загружающий библиотеки и использующий их функционал (main.cpp):
#include <stdio.h>
#include <dlfcn.h>
 
//gcc -o main main.cpp -ldl
 
int main() 
{    
    void *h1, *h2;
    // define function prototype
    typedef void (*foo_fn_ptr)(void);     
    // function pointers
    foo_fn_ptr foo1, foo2; 
    char *error;
 
    // open first library
    h1 = dlopen ("./libfoo1.so", RTLD_LAZY);
    if(!h1)
    {
        fputs(dlerror(), stderr);
        puts("");
        return 1;
    }
 
    // get function from first lib
    foo1 = (foo_fn_ptr) dlsym(h1, "foo");
    error = dlerror();
    if (error) 
    {
        puts(error);
        return 1;
    }
 
    // use function
    foo1();
 
    // load second lib
    h2 = dlopen ("./libfoo2.so", RTLD_LAZY);
    if(!h2)
    {
        fputs(dlerror(), stderr);
        puts("");
        return 1;
    }
 
    // get function from second lib
    foo2 = (foo_fn_ptr) dlsym(h2, "foo");
    error = dlerror();
    if (error) 
    {
        puts(error);
        return 1;
    }
 
    // use function
    foo2();
 
    // unload libraries
    dlclose(h1);
    dlclose(h2);
 
    return 0;
}
 
Как все это собирать. Сначала соберем наши shared objects:
gcc -Wall -Werror -o libfoo1.so -shared -fpic foo1.cpp
gcc -Wall -Werror -o libfoo2.so -shared -fpic foo2.cpp

Теперь соберем наш исполняемый файл, который загружает и использует эти библиотеки:
gcc -o main main.cpp -ldl

У нас все собрано и готово, надо только добавить папку, где лежат наши библиотеки в список путей, откуда их можно загружать:
export LD_LIBRARY_PATH=$PWD
Теперь можно запустить:
./main 
Hello world!
Init shared library 1
Hello, I'm a shared library 1
Init shared library 2
Hello, I'm a shared library 2
Deinit shared library 1
Deinit shared library 2
Ниже пояснения.

Код библиотек.
Зачем нам нужен extern "C"? Строго говоря, не нужен, потому что по умолчанию GCC (нынешняя версия, по крайней мере), экспортирует все функции и они доступны для использования, но есть один нюанс. Можно закомментировать эту строчку, пересобрать библиотеку и вместо увидеть ошибку "./libfoo2.so: undefined symbol: foo". То есть функция dlsym не находит символа "foo" в библиотеке, хотя мы точно знаем, что он там есть. С помощью nm мы просмотрим таблицу экспорта нашей библиотеки:
$ nm -D ./libfoo2.so
0000000000201028 B __bss_start
                 w __cxa_finalize
0000000000201028 D _edata
0000000000201030 B _end
00000000000006b4 T _fini
                 w __gmon_start__
0000000000000548 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w _Jv_RegisterClasses
                 U puts
0000000000000733 T _Z10foo_deinitv
0000000000000720 T _Z8foo_initv
00000000000006a0 T _Z3foov
$
Вот эта последняя строчка "_Z3foov" и есть имя нашей экспортированной функции и это очевидно не то же самое, что "foo". Теперь раскомментируем строчку extern "C", пересоберем библиотеку и запустим nm еще раз:
$ nm -D ./libfoo2.so
0000000000201028 B __bss_start
                 w __cxa_finalize
0000000000201028 D _edata
0000000000201030 B _end
000000000000075c T _fini
0000000000000746 T foo
                 w __gmon_start__
00000000000005d0 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w _Jv_RegisterClasses
                 U puts
0000000000000733 T _Z10foo_deinitv
0000000000000720 T _Z8foo_initv
$ 
Теперь мы видим "foo" и у нас все опять работает:
./main 
Hello world!
Init shared library 1
Hello, I'm a shared library 1
Init shared library 2
Hello, I'm a shared library 2
Deinit shared library 1
Deinit shared library 2

Это все связано с правилами наименования экспортируемых функций, можо погуглить "name mangling", а extern "C" принудительно заставляет компилятор именовать функции по простым и ясным правилам языка C.
Что же насчет __attribute__ ((visibility("default"))), то по умолчанию все функции и так имеют видимость default, но, во избежание всяких неожиданностей типа нестандартных флагов компилятора, мы добавляем этот флаг в каждую экспортируемую функцию.

Сборка библиотек.
Компилируем и создаем из объектного файла shared object двумя отдельными командами.
Компиляция:
gcc -c -Wall -Werror -fpic foo1.cpp
Все нюансы тут состоят в использовании флага pic (Position Independent Code), что необходимо для динамически загружаемых библиотек (статические библиотеки не нуждаются в таком флаге).
Сборка происходит так:
gcc -shared -o libfoo1.so foo1.o
gcc -Wall -Werror -o libfoo1.so -shared -fpic foo1.cpp

тут используется специальный флаг shared для создания динамических библиотек. То же самое одной командой:
gcc -Wall -Werror -o libfoo1.so -shared -fpic foo1.cpp

Сборка main.
Все как обычно, но линкуем библиотеку dl, которая содержит dlopen, dlsym, dlclose и прочие фукции:
gcc -o main main.cpp -ldl
Настройка окружения и запуск.
В linux библиотеки лежат в строго определенных местах и откуда-то еще их загрузить не получится (в отличии от Windows), поэтому, когда мы запустим программу в первый раз, мы увидим что-то такое:
$ ./main 
./libfoo1.so: cannot open shared object file: No such file or directory
$ 
Чтобы добавить новый путь, делаем следующее:
export LD_LIBRARY_PATH=$PWD

понедельник, 12 декабря 2016 г.

C++ command line helper

Иногда надо исполнить какую-то команду ОС. Это легко делается с помощью создания нового процесса и получение данных от него через пайп:

#include <iostream>
#include <stdio.h>
#include <memory>
 
bool run_shell_cmd(const std::string & cmd, std::string & outputStr)
{
    int closePipeResult = -1;
    // deleter to close pipe
    auto pipeDeleter = [&closePipeResult](FILE* pipe)
    {
        if(pipe != NULL)
            closePipeResult = pclose(pipe);
    };
    // create smart poinetr with custom deleter
    std::unique_ptr<FILE, decltype(pipeDeleter)> pipe(popen(cmd.c_str(), "r"), pipeDeleter);
 
    if(pipe.get() == NULL) 
        return false;
 
    // read output and save it to string
    char buffer[128];
    while (!feof(pipe.get()))
    {
        if (fgets(buffer, 128, pipe.get()) != NULL)
            outputStr += buffer;
    }
 
    // reset pointer to get result of pclose before exit of function
    pipe.reset();
 
    return (closePipeResult == 0);
}

среда, 7 декабря 2016 г.

Google Test Gmock

Предположим, мы разрабатываем клиента для некоего сервера, а сервер разрабатывается совсем другой командой и на данный момент он либо неработоспособен, либо вообще недоступен (очень частая ситуация на самом деле). Единственное, что у нас есть - это интерфейс этого сервера без имплементации в виде исходных файлов или бинарей. В таких случаях обычно делают какой-то простой сервер-заглушку, который по крайней мере позволяет проводить на нем отладку клиентской части. Это достаточно трудоемко и именно для того, чтобы избавить программиста от ненужной работы, и был создан gmock как часть Google Test Framework.
Лучше всего показать на примере.

// our server (we have interface only)
class IMyCalcServer
{
public:
    virtual int add(int a, int b) = 0;
    virtual int mult(int a, int b) = 0;
    virtual ~IMyCalcServer() {}
};
 
// client class
class MyClient
{
public:
    MyClient(IMyCalcServer * server) : srv(server) {}
    void doMath()
    {
        std::cout << "add " << srv->add(2, 2) << std::endl;
        std::cout << "mult " << srv->mult(1, 2) << std::endl;
    }
private:
    IMyCalcServer * srv;
};
 
// implementation of IMyCalcServer interface by gmock
class CalcServerMock : public IMyCalcServer
{
public:
    MOCK_METHOD2(add, int(int a, int b));
    MOCK_METHOD2(mult, int(int a, int b));
};
 
TEST(MyGmockTest, test1)
{
    // create instance of "InSequence" class to force gmock expect function calls in stict order
    ::testing::InSequence dummy;
 
    // here we create server instance, define calls order, parameters and return values
    CalcServerMock calcSrvMock;
    // we expect add() is called with parameters (2,2) and returns 4
    EXPECT_CALL(calcSrvMock, add(2,2)).WillOnce(testing::Return(4));
    // we expect mult() is called with parameters (2,2) and returns 4
    EXPECT_CALL(calcSrvMock, mult(2,2)).WillOnce(testing::Return(4));
 
    // do calculations
    MyClient cl(&calcSrvMock);
    cl.doMath();
}

четверг, 17 ноября 2016 г.

Использование is_base_of и enable_if и детектирование типов во время компиляции

Допустим у нас есть некая функция, которая должна быть определена только для наследников определенного класса. Можно сделать это в рантайме, но детектирование типов в рантайме это дорого и к тому же некрасиво. C++11 предлагает более красивое решение.
#include <iostream>
#include <type_traits>
 
class A 
{
public:
    virtual void do_work() = 0;
};
 
class B : public A 
{
public:
    void do_work() override { std::cout << "B::do_work()" << std::endl; };
};
 
class C
{
public:
    void do_work() { std::cout << "C::do_work()" << std::endl; };
};
 
template<typename T>
typename std::enable_if<std::is_base_of<A, T>::value, void>::type
execute_work(T &t)
{
    t.do_work();
}

UPD 03.12.2016:

можно сделать это гораздо проще:
//...
template<typename T>
bool execute_work(T &t)
{
    static_assert(std::is_base_of<A, T>::value, "T must be subclass of A");
    t.do_work();
}
 
int main()
{
    B b;
    C c;
    execute_work(b); //OK
    execute_work(c); //compile error
}
И теперь у нас компилятор будет выдавать именно то сообщение, которое мы указали в static_assert

четверг, 10 ноября 2016 г.

Передача определений прекомпиляции из cmake

Довольно частая задача, чтобы например подключать те или иные заголовки и делать это можно конечно только во время прекомпиляции.
Допустим есть такой CMakeLists.txt файл:

set(MYTARGET  "target")
# we have some string var that we want to convert to correct define for pre-compiler
set(INTERFACES_VERSION_STR  "-2016.10.23-" )

# remove all points (".")
string(REPLACE "."   ""   MY_INTERFACES_VER   ${INTERFACES_VERSION_STR})
# remove all "-" symbols
string(REPLACE "-"   ""   MY_INTERFACES_VER   ${MY_INTERFACES_VER})
# print them
message(STATUS "INTERFACES_VERSION_STR  ${INTERFACES_VERSION_STR}")
message(STATUS "MY_INTERFACES_VER   ${MY_INTERFACES_VER}")

add_library(${MYTARGET} my_source.cpp)

add_definitions(-DMY_INTERFACES_VER=${MY_INTERFACES_VER})

Тогда в исходном файле пишем:

//...
#if MY_INTERFACES_VER==20161023
#include "v20161023/include/interfaces.h"
    std::cout << "---- MY_INTERFACES_VER==20161023 ---" << std::endl;
#else
#include "mainline/include/interfaces.h"
    std::cout << "do some by default " << std::endl;
#endif


четверг, 12 мая 2016 г.

Far auto save option

Чтобы Far Manager открывал те же папки, которые были при закрытии, нужно просто поставить галочку напротив "Auto Save Setup" (F9 -> Options -> System Settings):



воскресенье, 22 ноября 2015 г.

Настройка рабочей среды для начала работы с OpenGL

Нарисовать раскрашенный треугольник, скопипастив пару строк кода из мануалов, значительно проще, чем подговить рабочее окружение. По крайней мере для того, кто с OpenGL сталкивается впервые.

Операционная система - Windows 7 x64 (она стоит на моем ноутбуке), а в качестве IDE выбран Visual Studio 2013 Express (очень удобный инструмент и доступен бесплатно).
Проблема в том, что нельзя просто взять и нарисовать треугольник.


Чтобы это сделать, нужно создать контекст, в котором мы будем рисовать и обрабатывать ввод-вывод. Для этого существуют специальные библиотеки. GLFW кажется мне достаточно подходящей для этой цели. Загружаем с сайта заголовочные файлы и бинарники. Но и этого недостаточно, потому что Visual Studio поставляется с заголовочными файлами для спецификации OpenGL 1.1, которая вышла ну очень давно, более 20 лет назад. Так что нам нужны еще свежие заголовочные файлы и я выбрал библиотеку GLEW, которая содержит не только свежие заголовочные файлы, но и еще много чего полезного.

Теперь приступаем к установке библиотек:
- распаковываем и скачанные архивы;
- копируем заголовочные файлы, чтобы Visual Studio мог ими пользоваться. В моем случае это было содержимое папок include в архивах, скопированное в папку "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include". Теперь там появились две новые папки с заголовочными файлами: GL и GLFW;
- копируем glfw3.lib, glew32.lib и glew32s.lib в "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\lib" (обратите внимание, что нужно использовать 32-битные версии этих файлов, т.к. Visual Studio генерирует 32-битные версии бинарных файлов);
- копируем glew32.dll и glfw3.dll в папку C:Windows\System (тоже используем 32-битные версии файлов).

Идем дальше. Открываем Visual Studio, создаем простое консольное приложение и настраиваем его: в "Configuration Properties => Linker => Input => Additional Dependencies" добавляем библиотеки glew32s.lib, glew32.lib, glfw3.lib, OpenGL32.lib:


Теперь можно попробовать запустить простой пример:
 
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
 
// Shader sources
const GLchar* vertexSource =
"#version 150 core\n"
"in vec2 position;"
"in vec3 color;"
"out vec3 Color;"
"void main() {"
"   Color = color;"
"   gl_Position = vec4(position, 0.0, 1.0);"
"}";
 
const GLchar* fragmentSource =
"#version 150 core\n"
"in vec3 Color;"
"out vec4 outColor;"
"void main() {"
"   outColor = vec4(Color, 1.0);"
"}";
 
int main(int argc, char* argv[])
{
 glfwInit();
 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
 glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
 
 glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
 
 GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL", nullptr, nullptr); // Windowed
 //GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL", glfwGetPrimaryMonitor(), nullptr); // Fullscreen
 
 glfwMakeContextCurrent(window);
 
 // Initialize GLEW
 glewExperimental = GL_TRUE;
 glewInit();
 
 // Create Vertex Array Object
 GLuint vao;
 glGenVertexArrays(1, &vao);
 glBindVertexArray(vao);
 
 // Create a Vertex Buffer Object and copy the vertex data to it
 GLuint vbo;
 glGenBuffers(1, &vbo);
 
 GLfloat vertices[] = {
  0.0f, 0.5f, 1.0f, 0.0f, 0.0f,
  0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
  -0.5f, -0.5f, 0.0f, 0.0f, 1.0f
 };
 
 glBindBuffer(GL_ARRAY_BUFFER, vbo);
 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
 
 // Create and compile the vertex shader
 GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
 glShaderSource(vertexShader, 1, &vertexSource, NULL);
 glCompileShader(vertexShader);
 
 // Create and compile the fragment shader
 GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
 glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
 glCompileShader(fragmentShader);
 
 // Link the vertex and fragment shader into a shader program
 GLuint shaderProgram = glCreateProgram();
 glAttachShader(shaderProgram, vertexShader);
 glAttachShader(shaderProgram, fragmentShader);
 glBindFragDataLocation(shaderProgram, 0, "outColor");
 glLinkProgram(shaderProgram);
 glUseProgram(shaderProgram);
 
 // Specify the layout of the vertex data
 GLint posAttrib = glGetAttribLocation(shaderProgram, "position");
 glEnableVertexAttribArray(posAttrib);
 glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), 0);
 
 GLint colAttrib = glGetAttribLocation(shaderProgram, "color");
 glEnableVertexAttribArray(colAttrib);
 glVertexAttribPointer(colAttrib, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (void*)(2 * sizeof(GLfloat)));
 
 //main loop
 while (!glfwWindowShouldClose(window))
 {
  if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
   glfwSetWindowShouldClose(window, GL_TRUE);
 
  // Clear the screen to black
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT);
 
  // Draw a triangle from the 3 vertices
  glDrawArrays(GL_TRIANGLES, 0, 3);
 
  glfwSwapBuffers(window);
  glfwPollEvents();
  //glfwShowWindow(window);
 }
 
 glDeleteProgram(shaderProgram);
 glDeleteShader(fragmentShader);
 glDeleteShader(vertexShader);
 
 glDeleteBuffers(1, &vbo);
 
 glDeleteVertexArrays(1, &vao);
 
 glfwTerminate();
 
 return 0;
}




Если все сделано правильно, мы должны увидеть такой результат:
Ссылки на использованные материалы:
Setting up Windows Environment Window and OpenGL context GLEW installation