plugins FEATURE manual loading of external plugins

Add lyplg_add() function to manually load external plugins (both
types and extensions) from a specific dynamic library.
diff --git a/src/plugins.c b/src/plugins.c
index fa243c8..e378773 100644
--- a/src/plugins.c
+++ b/src/plugins.c
@@ -18,6 +18,7 @@
 #include <assert.h>
 #include <dlfcn.h>
 #include <pthread.h>
+#include <stddef.h>
 #include <string.h>
 
 #include "common.h"
@@ -74,10 +75,35 @@
     int8_t plugin[];             /**< specific plugin type's data - ::lyplg_ext or ::lyplg_type */
 };
 
+static struct ly_set plugins_handlers = {0};
 static struct ly_set plugins_types = {0};
 static struct ly_set plugins_extensions = {0};
 
 /**
+ * @brief Just a variadic data to cover extension and type plugins by a single ::plugins_load() function.
+ *
+ * The values are taken from ::LY_PLUGINS_EXTENSIONS and ::LYPLG_TYPES macros.
+ */
+static const struct {
+    const char *id;          /**< string identifier: type/extension */
+    const char *apiver_var;  /**< expected variable name holding API version value */
+    const char *plugins_var; /**< expected variable name holding plugin records */
+    uint32_t apiver;         /**< expected API version */
+} plugins_load_info[] = {
+    {   /* LYPLG_TYPE */
+        .id = "type",
+        .apiver_var = "plugins_types_apiver__",
+        .plugins_var = "plugins_types__",
+        .apiver = LYPLG_TYPE_API_VERSION
+    }, {/* LYPLG_EXTENSION */
+        .id = "extension",
+        .apiver_var = "plugins_extensions_apiver__",
+        .plugins_var = "plugins_extensions__",
+        .apiver = LYPLG_EXT_API_VERSION
+    }
+};
+
+/**
  * @brief Iterate over list of loaded plugins of the given @p type.
  *
  * @param[in] type Type of the plugins to iterate.
@@ -151,7 +177,7 @@
         for (uint32_t i = 0; rec[i].name; i++) {
             LY_CHECK_RET(ly_set_add(&plugins_extensions, (void *)&rec[i], 0, NULL));
         }
-    } else { /* LY_PLUGIN_TYPE */
+    } else { /* LYPLG_TYPE */
         const struct lyplg_type_record *rec = (const struct lyplg_type_record *)recs;
 
         for (uint32_t i = 0; rec[i].name; i++) {
@@ -172,6 +198,7 @@
 
     ly_set_erase(&plugins_types, NULL);
     ly_set_erase(&plugins_extensions, NULL);
+    ly_set_erase(&plugins_handlers, (void(*)(void *))dlclose);
 }
 
 void
@@ -182,6 +209,104 @@
     pthread_mutex_unlock(&plugins_guard);
 }
 
+/**
+ * @brief Get the expected plugin objects from the loaded dynamic object and add the defined plugins into the lists of
+ * available extensions/types plugins.
+ *
+ * @param[in] dlhandler Loaded dynamic library handler.
+ * @param[in] pathname Path of the loaded library for logging.
+ * @param[in] type Type of the plugins to get from the dynamic library. Note that a single library can hold both types
+ * and extensions plugins implementations, so this function should be called twice (once for each plugin type) with
+ * different @p type values
+ * @return LY_ERR values.
+ */
+static LY_ERR
+plugins_load(void *dlhandler, const char *pathname, enum LYPLG type)
+{
+    const void *plugins;
+    uint32_t *version;
+
+    /* type plugin */
+    version = dlsym(dlhandler, plugins_load_info[type].apiver_var);
+    if (version) {
+        /* check version ... */
+        if (*version != plugins_load_info[type].apiver) {
+            LOGERR(NULL, LY_EINVAL, "Processing user %s plugin \"%s\" failed, wrong API version - %d expected, %d found.",
+                    plugins_load_info[type].id, pathname, plugins_load_info[type].apiver, *version);
+            return LY_EINVAL;
+        }
+
+        /* ... get types plugins information ... */
+        if (!(plugins = dlsym(dlhandler, plugins_load_info[type].plugins_var))) {
+            char *errstr = dlerror();
+            LOGERR(NULL, LY_EINVAL, "Processing user %s plugin \"%s\" failed, missing %s plugins information (%s).",
+                    plugins_load_info[type].id, pathname, plugins_load_info[type].id, errstr);
+            return LY_EINVAL;
+        }
+
+        /* ... and load all the types plugins */
+        LY_CHECK_RET(plugins_insert(type, plugins));
+    }
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+plugins_load_module(const char *pathname)
+{
+    LY_ERR ret = LY_SUCCESS;
+    void *dlhandler;
+    uint32_t types_count = 0, extensions_count = 0;
+
+    dlerror();    /* Clear any existing error */
+
+    dlhandler = dlopen(pathname, RTLD_NOW);
+    if (!dlhandler) {
+        LOGERR(NULL, LY_ESYS, "Loading \"%s\" as a plugin failed (%s).", pathname, dlerror());
+        return LY_ESYS;
+    }
+
+    if (ly_set_contains(&plugins_handlers, dlhandler, NULL)) {
+        /* the plugin is already loaded */
+        LOGVRB("Plugin \"%s\" already loaded.", pathname);
+
+        /* keep the correct refcount */
+        dlclose(dlhandler);
+        return LY_SUCCESS;
+    }
+
+    /* remember the current plugins lists for recovery */
+    types_count = plugins_types.count;
+    extensions_count = plugins_extensions.count;
+
+    /* type plugin */
+    ret = plugins_load(dlhandler, pathname, LYPLG_TYPE);
+    LY_CHECK_GOTO(ret, error);
+
+    /* extension plugin */
+    ret = plugins_load(dlhandler, pathname, LYPLG_EXTENSION);
+    LY_CHECK_GOTO(ret, error);
+
+    /* remember the dynamic plugin */
+    ret = ly_set_add(&plugins_handlers, dlhandler, 1, NULL);
+    LY_CHECK_GOTO(ret, error);
+
+    return LY_SUCCESS;
+
+error:
+    dlclose(dlhandler);
+
+    /* revert changes in the lists */
+    while (plugins_types.count > types_count) {
+        ly_set_rm_index(&plugins_types, plugins_types.count - 1, NULL);
+    }
+    while (plugins_extensions.count > extensions_count) {
+        ly_set_rm_index(&plugins_extensions, plugins_extensions.count - 1, NULL);
+    }
+
+    return ret;
+}
+
 LY_ERR
 lyplg_init(void)
 {
@@ -230,3 +355,26 @@
     }
     return ret;
 }
+
+API LY_ERR
+lyplg_add(const char *pathname)
+{
+    LY_ERR ret = LY_SUCCESS;
+
+    LY_CHECK_ARG_RET(NULL, pathname, LY_EINVAL);
+
+    /* works only in case a context exists */
+    pthread_mutex_lock(&plugins_guard);
+    if (!context_refcount) {
+        /* no context */
+        pthread_mutex_unlock(&plugins_guard);
+        LOGERR(NULL, LY_EDENIED, "To add a plugin, at least one context must exists.");
+        return LY_EDENIED;
+    }
+
+    ret = plugins_load_module(pathname);
+
+    pthread_mutex_unlock(&plugins_guard);
+
+    return ret;
+}
diff --git a/src/plugins.h b/src/plugins.h
index 7baa00f..13ed304 100644
--- a/src/plugins.h
+++ b/src/plugins.h
@@ -35,6 +35,22 @@
     LYPLG_EXTENSION  /**< YANG extension */
 };
 
+/**
+ * @brief Manually load a plugin file.
+ *
+ * Note, that a plugin can be loaded only if there is at least one context. The loaded plugins are connected with the
+ * existence of a context. When all the contexts are destroyed, all the plugins are unloaded.
+ *
+ * @param[in] pathname Path to the plugin file. It can contain types or extensions plugins, both are accepted and correctly
+ * loaded.
+ *
+ * @return LY_SUCCESS if the file contains valid plugin compatible with the library version.
+ * @return LY_EDENIED in case there is no context and the plugin cannot be loaded.
+ * @return LY_EINVAL when pathname is NULL or the plugin contains invalid content for this libyang version.
+ * @return LY_ESYS when the plugin file cannot be loaded.
+ */
+LY_ERR lyplg_add(const char *pathname);
+
 /** @} plugins */
 
 #ifdef __cplusplus
diff --git a/src/schema_compile_node.c b/src/schema_compile_node.c
index 24187ad..b2cc259 100644
--- a/src/schema_compile_node.c
+++ b/src/schema_compile_node.c
@@ -1738,6 +1738,29 @@
     return ret;
 }
 
+/**
+ * @brief Find the correct plugin implementing the described type
+ *
+ * @param[in] mod Module where the type is defined
+ * @param[in] name Name of the type (typedef)
+ * @param[in] basetype Type's basetype (when the built-in base plugin is supposed to be used)
+ * @return Pointer to the plugin implementing the described data type.
+ */
+static struct lyplg_type *
+lys_compile_type_get_plugin(struct lys_module *mod, const char *name, LY_DATA_TYPE basetype)
+{
+    struct lyplg_type *p;
+
+    /* try to find loaded user type plugins */
+    p = lyplg_find(LYPLG_TYPE, mod->name, mod->revision, name);
+    if (!p) {
+        /* use the internal built-in type implementation */
+        p = lyplg_find(LYPLG_TYPE, "", NULL, ly_data_type2str[basetype]);
+    }
+
+    return p;
+}
+
 LY_ERR
 lys_compile_type(struct lysc_ctx *ctx, struct lysp_node *context_pnode, uint16_t context_flags, const char *context_name,
         struct lysp_type *type_p, struct lysc_type **type, const char **units, struct lysp_qname **dflt)
@@ -1937,8 +1960,7 @@
         }
 
         (*type)->basetype = basetype;
-        /* TODO user type plugins */
-        (*type)->plugin = lyplg_find(LYPLG_TYPE, "", NULL, ly_data_type2str[basetype]);
+        (*type)->plugin = lys_compile_type_get_plugin(tctx->tpdf->type.pmod->mod, tctx->tpdf->name, basetype);
         prev_type = *type;
         ret = lys_compile_type_(ctx, tctx->node, tctx->tpdf->flags, tctx->tpdf->name,
                 &((struct lysp_tpdf *)tctx->tpdf)->type, basetype, tctx->tpdf->name, base, type);
@@ -1952,8 +1974,7 @@
     if (type_p->flags || !base || (basetype == LY_TYPE_LEAFREF)) {
         /* get restrictions from the node itself */
         (*type)->basetype = basetype;
-        /* TODO user type plugins */
-        (*type)->plugin = lyplg_find(LYPLG_TYPE, "", NULL, ly_data_type2str[basetype]);
+        (*type)->plugin = base ? base->plugin : lyplg_find(LYPLG_TYPE, "", NULL, ly_data_type2str[basetype]);
         ++(*type)->refcount;
         ret = lys_compile_type_(ctx, context_pnode, context_flags, context_name, type_p, basetype, NULL, base, type);
         LY_CHECK_GOTO(ret, cleanup);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 8322351..8735388 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -24,7 +24,7 @@
 
     # Set common attributes of all tests
     set_target_properties(${TEST_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests")
-    target_link_libraries(${TEST_NAME} ${CMOCKA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${PCRE2_LIBRARIES} m)
+    target_link_libraries(${TEST_NAME} ${CMOCKA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${PCRE2_LIBRARIES} ${CMAKE_DL_LIBS} m)
     if (NOT APPLE)
         if (ADDTEST_WRAP)
             set_target_properties(${TEST_NAME} PROPERTIES LINK_FLAGS "${ADDTEST_WRAP}")
diff --git a/tests/ld.supp b/tests/ld.supp
index 53876b6..e501a3b 100644
--- a/tests/ld.supp
+++ b/tests/ld.supp
@@ -1,10 +1,8 @@
 {
-   <insert_a_suppression_name_here>
+   dl's thread-local data
    Memcheck:Leak
    match-leak-kinds: reachable
    fun:calloc
    fun:_dlerror_run
    fun:dlopen@@GLIBC_2.2.5
-   fun:lyext_load_plugins
-   fun:ly_ctx_new
 }