Skip to content

Commit

Permalink
Spike implementation of the module loader design
Browse files Browse the repository at this point in the history
  • Loading branch information
avranju committed Nov 14, 2016
1 parent a1df1de commit 70ce931
Show file tree
Hide file tree
Showing 26 changed files with 1,843 additions and 458 deletions.
8 changes: 8 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
root = true

[*]
insert_final_newline = true # A newline ending in every file
indent_style = space # We use spaces. Period.
indent_size = 4 # And 4 of them.
charset = utf-8 # Save files as utf-8
trim_trailing_whitespace = true # no trailing whitespace in lines
4 changes: 2 additions & 2 deletions bindings/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ endif()
if(${enable_java_binding})
add_subdirectory(java)
endif()

if(${enable_nodejs_binding})
add_subdirectory(nodejs)
endif()
endif()
5 changes: 3 additions & 2 deletions bindings/nodejs/inc/nodejs.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#define NODEJS_H

#include "module.h"
#include "azure_c_shared_utility/strings.h"

#ifdef __cplusplus
extern "C"
Expand All @@ -13,8 +14,8 @@ extern "C"

typedef struct NODEJS_MODULE_CONFIG_TAG
{
const char* main_path;
const char* configuration_json;
STRING_HANDLE main_path;
STRING_HANDLE configuration_json;
}NODEJS_MODULE_CONFIG;

MODULE_EXPORT const MODULE_API* MODULE_STATIC_GETAPI(NODEJS_MODULE)(const MODULE_API_VERSION gateway_api_version);
Expand Down
92 changes: 17 additions & 75 deletions bindings/nodejs/src/nodejs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "azure_c_shared_utility/xlogging.h"
#include "azure_c_shared_utility/threadapi.h"
#include "azure_c_shared_utility/agenttime.h"
#include "azure_c_shared_utility/strings.h"
#include "parson.h"

#include "module.h"
Expand Down Expand Up @@ -97,8 +98,8 @@ static MODULE_HANDLE NODEJS_Create(BROKER_HANDLE broker, const void* configurati
NODEJS_MODULE_HANDLE_DATA handle_data_input
{
broker,
module_config->main_path,
module_config->configuration_json,
STRING_c_str(module_config->main_path),
STRING_c_str(module_config->configuration_json),
on_module_start
};

Expand Down Expand Up @@ -129,80 +130,21 @@ static MODULE_HANDLE NODEJS_Create(BROKER_HANDLE broker, const void* configurati
return result;
}

static MODULE_HANDLE NODEJS_CreateFromJson(BROKER_HANDLE broker, const char* configuration)
static void* NODEJS_ParseConfigurationFromJson(const char* configuration)
{
MODULE_HANDLE result;
return (void*)STRING_construct(configuration);
}

/*Codes_SRS_NODEJS_05_001: [ NODEJS_CreateFromJson shall return NULL if broker is NULL. ]*/
/*Codes_SRS_NODEJS_05_002: [ NODEJS_CreateFromJson shall return NULL if configuration is NULL. ]*/
if (broker == NULL || configuration == NULL)
static void NODEJS_FreeConfiguration(void* configuration)
{
if (configuration == NULL)
{
LogError("NULL parameter detected broker=%p configuration=%p", broker, configuration);
result = NULL;
LogError("configuration is NULL");
}
else
{
/*Codes_SRS_NODEJS_05_012: [ NODEJS_CreateFromJson shall parse configuration as a JSON string. ]*/
JSON_Value* json = json_parse_string((const char*)configuration);
if (json == NULL)
{
/*Codes_SRS_NODEJS_05_003: [ NODEJS_CreateFromJson shall return NULL if configuration is not a valid JSON string. ]*/
LogError("unable to json_parse_string");
result = NULL;
}
else
{
JSON_Object* obj = json_value_get_object(json);
if (obj == NULL)
{
/*Codes_SRS_NODEJS_05_014: [ NODEJS_CreateFromJson shall return NULL if the configuration JSON does not start with an object at the root. ]*/
LogError("unable to json_value_get_object");
result = NULL;
}
else
{
/*Codes_SRS_NODEJS_05_013: [ NODEJS_CreateFromJson shall extract the value of the main_path property from the configuration JSON. ]*/
const char* main_path = json_object_get_string(obj, "main_path");
if (main_path == NULL)
{
/*Codes_SRS_NODEJS_05_004: [ NODEJS_CreateFromJson shall return NULL if the configuration JSON does not contain a string property called main_path. ]*/
LogError("json_object_get_string failed");
result = NULL;
}
else
{
/*Codes_SRS_NODEJS_05_006: [ NODEJS_CreateFromJson shall extract the value of the args property from the configuration JSON. ]*/
JSON_Value* args = json_object_get_value(obj, "args"); // args is allowed to be NULL
char* args_str = json_serialize_to_string(args);

NODEJS_MODULE_CONFIG config =
{
main_path, args_str
};

/*Codes_SRS_NODEJS_05_005: [ NODEJS_CreateFromJson shall populate a NODEJS_MODULE_CONFIG object with the values of the main_path and args properties and invoke NODEJS_Create passing the broker handle and the config object. ]*/
result = NODEJS_Create(broker, (const void*)&config);
if (result == NULL)
{
/*Codes_SRS_NODEJS_05_008: [ If NODEJS_Create fail then the value NULL shall be returned. ]*/
LogError("Unable to create Node JS module");
// return 'result' as-is
}
else
{
/*Codes_SRS_NODEJS_05_007: [ If NODEJS_Create succeeds then a valid MODULE_HANDLE shall be returned. ]*/
// return 'result' as-is
}

free(args_str);
}
}

json_value_free(json);
}
STRING_delete((STRING_HANDLE)configuration);
}

return result;
}

static bool validate_input(BROKER_HANDLE broker, const NODEJS_MODULE_CONFIG* module_config)
Expand All @@ -223,21 +165,21 @@ static bool validate_input(BROKER_HANDLE broker, const NODEJS_MODULE_CONFIG* mod
else if (
module_config->configuration_json != NULL
&&
(json = json_parse_string(module_config->configuration_json)) == NULL
(json = json_parse_string(STRING_c_str(module_config->configuration_json))) == NULL
)
{
LogError("Unable to parse configuration JSON");
result = false;
}
else if(
#ifdef WIN32
_access(module_config->main_path, 4) != 0
_access(STRING_c_str(module_config->main_path), 4) != 0
#else
access(module_config->main_path, 4) != 0
access(STRING_c_str(module_config->main_path), 4) != 0
#endif
)
{
LogError("Unable to access the JavaScript file at path '%s'", module_config->main_path);
LogError("Unable to access the JavaScript file at path '%s'", STRING_c_str(module_config->main_path));
result = false;
}
else
Expand Down Expand Up @@ -1359,14 +1301,14 @@ static const MODULE_API_1 NODEJS_APIS_all =
{
{MODULE_API_VERSION_1},

NODEJS_CreateFromJson,
NODEJS_ParseConfigurationFromJson,
NODEJS_FreeConfiguration,
NODEJS_Create,
NODEJS_Destroy,
NODEJS_Receive,
NODEJS_Start
};


#ifdef BUILD_MODULE_TYPE_STATIC
MODULE_EXPORT const MODULE_API* MODULE_STATIC_GETAPI(NODEJS_MODULE)(const MODULE_API_VERSION gateway_api_version)
#else
Expand Down
2 changes: 1 addition & 1 deletion core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,4 @@ install(
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}ConfigVersion.cmake"
DESTINATION
${package_location}
)
)
64 changes: 30 additions & 34 deletions core/devdoc/module_loaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@ design goals of the Gateway SDK is the idea that the SDK will allow for
flexibility with respect to how a module is packaged and distributed and loaded
in the gateway. This document describes at a high level how this works. Gateway
modules can be written using different technology stacks. At the time of writing
modules can be written using C, .NET, Node.js and Java. The responsibility of
modules can be written using C, .NET, Node.js or Java. The responsibility of
bootstrapping the respective runtimes (in case of stacks that have a runtime
that is) and loading the module code lies with the module loader.
that is) and loading the module code lies with the *module loader*.

What is a Module Loader?
------------------------

The primary duty of a module loader in the gateway is to locate and load a
module - or in other words, to abstract away from the gateway the details of
locating and loading a module. A gateway module maybe native or managed (.NET,
locating and loading a module. A gateway module may be native or managed (.NET,
Node.js or Java). If its a managed module then the loader is responsible for
ensuring that the runtime needed to successfully load and run the module is
loaded and initialized first.

From the perspective of the gateway, a module loader is a piece of code that
knows how to load a module implementation and hand the gateway a table of
function pointers that contain the module implementation. The gateway doesn’t
really care how the module loader goes about acquiring the said pointers.
knows how to load a module instance and hand the gateway a table of function
pointers used to control it. The gateway doesn’t really care how the module
loader goes about acquiring the said pointers.

Loader Configuration
--------------------
Expand All @@ -38,22 +38,23 @@ A loader is defined by the following attributes:

- **Name**: A string that can be used to reference this particular loader

- **Module Path**: Optional path to a language binding implementation for
non-native loaders
- **Binding Module Path**: Optional path to a language binding implementation
for non-native loaders

- **Configuration**: Optional additional configuration parameters that may be
used to configure the runtime that is to be loaded
used to configure the runtime that is to be loaded. For example, one might
specify custom runtime options for the Java Virtual Machine here.

When initializing a gateway from a JSON configuration file via the
`Gateway_CreateFromJson` API, the loader configuration can be specified via the
top level `loaders` array. For example:
`Gateway_CreateFromJson` function, the loader configuration can be specified via
the top level `loaders` array. For example:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
{
"loaders": [
{
"type": "java",
"name": "java_loader",
"name": "java",
"jvm.options": {
... jvm options here ...
}
Expand Down Expand Up @@ -85,16 +86,16 @@ The Gateway SDK ships with a set of loaders that support dynamically loaded
modules and language binding loaders for .NET, Node.js and Java. They can be
referenced using the following loader names:

- `native_loader`: This implements loading of native modules - that is, plain
C modules.
- `native`: This implements loading of native modules - that is, plain C
modules.

- `java_loader`: This implements loading of modules implemented using the Java
programming language.
- `java`: This implements loading of modules implemented using the Java
programming language jand compiled to JAR files.

- `node_loader`: This implements loading of modules implemented using Node.js.
- `node`: This implements loading of modules implemented using Node.js.

- `dotnet_loader`: This implements loading of modules implemented using a .NET
language.
- `dotnet`: This implements loading of modules implemented using a .NET
language and compiled as a .NET assembly.

It is legal to completely omit specifying the `loaders` array in which case the
default loaders specified above will be made available using default options.
Expand All @@ -106,7 +107,7 @@ Here’s an example of a module configuration that makes use of the Java loader:
{
"name": "printer",
"loader": {
"name": "java_loader",
"name": "java",
"class.name": "org.contoso.gateway.module.Printer",
"class.path": "./printer.jar"
}
Expand Down Expand Up @@ -171,6 +172,8 @@ Briefly, here’s how the gateway bootstraps itself:
2. Module loaders for a given language binding may or may not be available
depending on what *CMake* build options were used when the source was built.
For example if CMake was run with the `enable_dotnet_binding` variable set
to `false`/`off` then the corresponding loader becomes unavailable for use.
3. Every module loader implementation provides a global function that is
responsible for returning a pointer to a `MODULE_LOADER` instance. The
Expand Down Expand Up @@ -212,32 +215,25 @@ Briefly, here’s how the gateway bootstraps itself:
loader then it has the effect of overriding the default loader.
6. When initializing custom loaders, if the caller has provided a value in
`MODULE_LOADER::binding_module_path` then we proceed to make use of it. If
there’s no `binding_module_path` provided then the gateway looks for a
DLL/SO with a well-known name by invoking the underlying operating system’s
default DLL/SO search algorithm. We also search in the folder where the
gateway binary was run from and from the `./bin` folder. If the module
`MODULE_LOADER::binding_module_path` then the gateway proceeds to make use
of it. If there’s no `binding_module_path` provided then the gateway looks
for a DLL/SO with a well-known name by invoking the underlying operating
system’s default DLL/SO search algorithm. We also search in the folder where
the gateway binary was run from and from the `./bin` folder. If the module
cannot be located at the end of this search then it is an error.
7. In case of a *native* module loader, there is no binding module. In order to
avoid writing specialized code for just this case we will have a shim
*binding* module where the module API implementation simply delegates to the
*real* module’s API.
8. The gateway calls the module loader’s `Load` callback to acquire a
8. The gateway calls the module loader’s `Load` function to acquire a
`MODULE_LIBRARY_HANDLE`. Each module loader’s implementation of this
function would do what’s necessary to also load the associated runtime (in
case of language binding modules).
9. The language binding module loader implementations will use the dynamic
DLL/SO loading API to load the binding module DLLs/SOs.
loader to load the binding module DLLs/SOs.
10. The gateway then proceeds to acquire the module’s function pointer table and
calls `Module_Create` or `Module_CreateFromJson` to instantiate the module.
In case of language binding modules this callback would be on the binding
module itself.
 
-
 
34 changes: 25 additions & 9 deletions core/inc/module.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,30 @@ extern "C"
MODULE_HANDLE module_handle;
};

/** @brief Creates an instance of a module.
/** @brief Translates module configuration from a JSON string to a
* module specific data structure.
*
* @details This function is optional.
* @details This function must be implemented by modules that support
* configuration options.
*
* @param broker The #BROKER_HANDLE to which this module
* will publish messages.
* @param configuration A JSON string which describes any needed
* configuration to create this module.
*
* @return A non-NULL #MODULE_HANDLE upon success, otherwise @c NULL.
* @return A void pointer containing a parsed representation of the
* module's configuration.
*/
typedef MODULE_HANDLE(*pfModule_CreateFromJson)(BROKER_HANDLE broker, const char* configuration);
typedef void*(*pfModule_ParseConfigurationFromJson)(const char* configuration);

/** @brief Frees the configuration object returned by the
* ParseConfigurationFromJson function.
*
* @details This function must be implemented by modules that support
* configuration options.
*
* @param configuration A void pointer containing a parsed representation
* of the module's configuration.
*/
typedef void(*pfModule_FreeConfiguration)(void* configuration);

/** @brief Creates an instance of a module.
*
Expand Down Expand Up @@ -137,9 +149,13 @@ extern "C"
/** @brief Always the first element on a Module's API*/
MODULE_API base;

/** @brief Function pointer to the #Module_CreateFromJson function
* (optional). */
pfModule_CreateFromJson Module_CreateFromJson;
/** @brief Function pointer to the #Module_ParseConfigurationFromJson
* function. */
pfModule_ParseConfigurationFromJson Module_ParseConfigurationFromJson;

/** @brief Function pointer to the #Module_FreeConfiguration
* function. */
pfModule_FreeConfiguration Module_FreeConfiguration;

/** @brief Function pointer to the #Module_Create function. */
pfModule_Create Module_Create;
Expand Down
Loading

0 comments on commit 70ce931

Please sign in to comment.