LINUX.ORG.RU

Управление контекстом инициализации «трамплинных» библиотек

 , ,


2

5

По работе часто приходится писать вещи, чем-то похожие на glew.

То есть, необходимо работать с динамически-загружаемыми библиотеками, используя указатели на ее функции, посредством вызова пары функций вроде dlopen/dlsym для сабжа или LoadLibrary / GetProcAddress для оффтопика.

Например, в такой способ организована в боевом коде работа с OpenCL ICD, NVML, NVRTC, CUDA Driver API итд.

Хотелось бы организовать некий «контекст инициализации», то есть вызывать dlsym не для всех функций спецификации OpenCL 1.2, а только тех, которые потом используются приложением.

Если вы знаете примеры программ в которых организована такая ‘хотелка’, или сами делали что-то подобное - поделитесь опытом.

В данный момент думаю в качестве «объекта контекста инициализации» использовать ассоциативный массив вроде ID -> имя_нужной_функции.

Интересует также способы как-то автоматизировать парсинг клиентского кода, для того чтоб правильно сформировать этот ассоциативный массив (контекст инициализации).

Может вы знаете литературу в которой описано что-то подходящее, потому как городить Visitor из банды 4х или из Александресочки кажется небольшим оверкилом.

по-моему, ты описал обычные экспортные (ну или «импортные») библиотеки (которые lib). правда, ныне они непопулярны. хотя ничего плохого в них не было.

Iron_Bug ★★★★ ()

Может libtool поможет?

Или позднее связывание?

В Линуксах обычно dlsym() может делаться автоматически, когда к конкрентной функции произошол вызов.

Обычно это чаще мешает чем помогает, но возможно это именно то что тебе нужно.

cvv ★★★★★ ()

В данный момент думаю в качестве «объекта контекста инициализации» использовать ассоциативный массив вроде ID -> имя_нужной_функции.

Будет достаточно неторопливо. Лучше структуру с полями. И парсинг тогда будет автоматически компилятором.

monk ★★★★★ ()

Попробую объяснить на примере с кодом, как есть, и чего хотелось бы достичь.

Работа с OpenCL организована таким образом: в ocl_defs.h находятся описания структур и битовых полей из OpenCL, также есть ocl_api.h следующего содержания:

#include "ocl_defs.h"

typedef CL_API_ENTRY cl_int CL_API_CALL tclGetPlatformIDs(cl_uint, cl_platform_id*, cl_uint*);
typedef CL_API_ENTRY cl_int CL_API_CALL tclGetPlatformInfo(cl_platform_id, cl_platform_info, size_t, void*, size_t*);
typedef CL_API_ENTRY cl_int CL_API_CALL tclGetDeviceIDs(cl_platform_id, cl_device_type, cl_uint, cl_device_id*, cl_uint*);
typedef CL_API_ENTRY cl_int CL_API_CALL tclGetDeviceInfo(cl_device_id, cl_device_info, size_t, void *, size_t *);
/* и так далее описаны типы функций из OpenCL 1.2 */

Пользовательские подсистемы, которые хотят использовать OpenCL для своих вычислений, подключают файл ocl_trampoline.h:

#include "ocl_api.h"

extern tclGetPlatformIDs   *clGetPlatformIDs;
extern tclGetPlatformInfo  *clGetPlatformInfo;
extern tclGetDeviceIDs     *clGetDeviceIDs;
extern tclGetDeviceInfo    *clGetDeviceInfo;

/* далее описаны указатели на остальные функции из OpenCL 1.2 */

extern int CL_API_CALL OpenCLRuntimeDynload();

Таким образом, подключив файл ocl_trampoline.h, пользовательский код использует точно такие же имена функций, как и в стандарте OpenCL без необходимости связываться (линковаться) с OpenCL ICD, так как работает с указателями-трамплинами имеющими такие же имена.

Перед всякой работой связанной с реальными вызовами функций из OpenCL, пользовательский код обязан вызвать функцию OpenCLRuntimeDynload(), чтоб быть уверенным в том, что указатели 'имитирующие' функции, были инициализированы правильными адресами.

OpenCLRuntimeDynload() как раз и содержит в себе пары dlopen/dlsym:


tclGetPlatformIDs *clGetPlatformIDs = nullptr;
tclGetPlatformInfo *clGetPlatformInfo = nullptr;
tclGetDeviceIDs *clGetDeviceIDs = nullptr;
tclGetDeviceInfo *clGetDeviceInfo = nullptr;
/* далее обнуляются и остальные указатели,
  есть также опциональная функция, которая их все обнуляет.*/

int CL_API_CALL OpenCLRuntimeDynload()
{
  OCLDynloadError dl_err = le_OK;

#define QUOTE(x) #x

#if (OCL_TRAMPOLINE_OS == OCL_TRAMPOLINE_WIN)
#define SET_PROC_ADDR(name) name = reinterpret_cast<t##name *>(GetProcAddress(reinterpret_cast<HMODULE>(ocl_lib_handle_instance().get()), QUOTE(name))); if (!name) { dl_err = le_##name; break; }
#elif (OCL_TRAMPOLINE_OS == OCL_TRAMPOLINE_UNIX)
#define SET_PROC_ADDR(name) name = reinterpret_cast<t##name *>(dlsym(ocl_lib_handle_instance().get(), QUOTE(name))); if (!name) { dl_err = le_##name; break; }
#endif

  for(;;) {
    if (!ocl_lib_handle_instance()) {
      std::unique_lock<std::mutex> lock(ocl_lib_handle_mutex_instance());

#if(OCL_TRAMPOLINE_OS == OCL_TRAMPOLINE_WIN)
      ocl_lib_handle_instance().reset(reinterpret_cast<void*>(LoadLibraryW(L"OpenCL.dll")));
#elif (OCL_TRAMPOLINE_OS == OCL_TRAMPOLINE_UNIX)
      ocl_lib_handle_instance().reset(dlopen("libOpenCL.so", RTLD_LAZY));
#endif
    }

    if (!ocl_lib_handle_instance()) {
      dl_err = le_ICD_LIB_NOT_FOUND;
      break;
    }

    SET_PROC_ADDR(clGetPlatformIDs);
    SET_PROC_ADDR(clGetPlatformInfo);
    SET_PROC_ADDR(clGetDeviceIDs);
    SET_PROC_ADDR(clGetDeviceInfo);
    /* далее устанавливаем адреса всех оставшихся OpenCL 1.2 функций */
    
    break;
  }

  int32_t res = PXP_OK; /* унифицированный код ошибки */

  /* если во время dlsym что-то пошло не так - формируем
     унифицированный код ошибки, содержащий коды
     подсистемы (facility) и собственно ошибки (error code), в виде
     severity | facility | error_code, в данном случае
     Error | OpenCLDynloadSubsystem | SomeFnNotFound
   */
  if (dl_err != le_OK)
    res = DOCLDL_MAKE_ERROR(dl_err);

  return res;

#undef QUOTE
#undef SET_PROC_ADDR
}

Хотелось бы соорудить некую сущность - «контекст инициализации», передав ее параметром в функцию OpenCLRuntimeDynload() быть уверенным в том, что инициализированы только те функции, которые в данной подсистеме будут использованы а не вообще весь набор.

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

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

Как достичь цели с помощью «структуры с полями» я пока не совсем понимаю, так как структура будет каждый раз иметь разный тип, если ее использовать в качестве параметра для OpenCLRuntimeDynload().

Возможно нагородил тут все слишком сложно и неправильно, то интересно как сделать проще и правильнее по-другому. Вдохновлялся примерами из CUDA для их работы с их Driver API, когда сочинил такое.

Возможно Vulkan со своими validation layers имеет нужную мне архитектуру и решил бы мои хотелки, но пока с ним слабо знаком.

Понимаю, что изобретаю велосипед, который раз и навсегда решит проблему dll-hell но то такое, может кто изобрел получше :)

nikitos ★★ ()
Последнее исправление: nikitos (всего исправлений: 1)
Ответ на: комментарий от nikitos

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

Но в рамках одной программы имеет смысл инициализировать dlsym один раз, а не по разу для подсистемы.

Как достичь цели с помощью «структуры с полями» я пока не совсем понимаю, так как структура будет каждый раз иметь разный тип, если ее использовать в качестве параметра для OpenCLRuntimeDynload().

Так у тебя .h обязательно без изменений? Предполагалось что-то вроде:

struct tCL {
  // generated begin
  tclGetPlatformIDs   *clGetPlatformIDs;
  tclGetPlatformInfo  *clGetPlatformInfo;
  tclGetDeviceIDs     *clGetDeviceIDs;
  tclGetDeviceInfo    *clGetDeviceInfo;  
  // generated end
} CL;

В OpenCLRuntimeDynload аналогично генерируется блок c SET_PROC_ADDR.

В программе функции используются через структуру. Например, CL.clGetPlatformIDs(…).

При компиляции компилятор выдаст ошибки для всех неопределённых полей. Из ошибок собираем список и добавляем в .h и OpenCLRuntimeDynload.

monk ★★★★★ ()
Ответ на: комментарий от nikitos

Понимаю, что изобретаю велосипед, который раз и навсегда решит проблему dll-hell

Нет, не понимаешь.

интересно как сделать проще и правильнее по-другому.

Условную компиляцию, если всё в рамках одного исполняемого образа. Если подсистемы в разных модулях, то вобще ничего не надо.

LamerOk ★★★★★ ()
Ответ на: комментарий от Iron_Bug

ты описал обычные экспортные (ну или «импортные») библиотеки (которые lib)

Именно их. На Unix-подобных системах они обычно не реализованы, поэтому людям приходится вручную городить библиотеки трамплинов типа glew. В другом комменте я указал мини-тул, который отчасти автоматизирует генерацию boilerplate-кода.

yugr ()
Ответ на: комментарий от yugr

implib была тулза у боланда во времена 32-битных винд, когда для fastcall dll-ки от билдера нужно было сгенерировать либку для stdcall visual studio.

Возможно, решение интересное, посмотрю спасибо.

nikitos ★★ ()
Ответ на: комментарий от yugr

После беглого просмотра readme.md на гитхабе и перехода к вопросу на SO, корторый послужил вдохновителем для написания implib.so, думаю что мне подходит некоторые элементы решения из одного из ответов на этот вопрос.

«using a constructor function in your plugin», идея похожая на ту, что обозвал «контекстом инициализации».

nikitos ★★ ()
Последнее исправление: nikitos (всего исправлений: 1)
Ответ на: комментарий от nikitos

В винде просто не хочется изобретать велосипед т.к. фичи (ленивая загрузка, коллбэки для выбора версии библиотеки и т.п.) уже поддержаны в стандартном тулчейне.

yugr ()