Skip to content

4. Standards: Dynamic Library Loading for Vendor Abstraction

Ulrond edited this page Nov 7, 2024 · 1 revision

Dynamic Library Loading for Vendor Abstraction

Description

This example demonstrates how to use dynamic library loading to abstract vendor-specific code in a Hardware Abstraction Layer (HAL). This approach allows you to:

  • Remove build time dependancies isolating vendor specific library requirements into the wrapper.
  • Maintain a consistent interface for interacting with hardware from different vendors.
  • Improve code modularity and maintainability by separating vendor-specific code from the HAL.
// --- hal_interface.h ---

#ifndef HAL_INTERFACE_H
#define HAL_INTERFACE_H

// Define the HAL interface (concrete functions). 
// These functions provide a consistent interface to the application, 
// regardless of the underlying vendor implementation.
int hal_init(void);                      // Initialise the HAL
int hal_read_sensor(int sensor_id);      // Read from a sensor using the HAL
void hal_write_actuator(int actuator_id, int value); // Write to an actuator using the HAL

#endif // HAL_INTERFACE_H

// --- hal_impl.c ---

#include "hal_interface.h"
#include <dlfcn.h>
#include <stdio.h>

// Define the vendor interface. This structure holds function pointers
// to functions that will be implemented in the vendor-specific library.
typedef struct 
{
  int (*vendor_init)(void);              // Initialise the vendor hardware
  int (*vendor_read_sensor)(int sensor_id);  // Read data from a sensor
  void (*vendor_write_actuator)(int actuator_id, int value); // Write a value to an actuator
} vendor_interface_t;

// Function to load a shared library at runtime.
// This function uses dlopen to load the specified library file.
void* load_dependency(const char* dependency_path) 
{
  void* handle = dlopen(dependency_path, RTLD_LAZY);
  if (!handle) {
    fprintf(stderr, "Error loading dependency: %s\n", dlerror());
    return NULL;
  }
  return handle;
}

// Global variable to hold the vendor interface. This variable will 
// store the function pointers loaded from the vendor library.
vendor_interface_t vendor;

// Function to initialize the HAL and load the vendor library.
// This function is responsible for loading the vendor-specific 
// library and initializing the HAL with the vendor's functions.
int hal_init(void) 
{
  // Load the vendor library (e.g., vendor_lib.so)
  void* vendor_lib_handle = load_dependency("./vendor_lib.so");
  if (!vendor_lib_handle) 
  {
    return -1;
  }

  // Get the 'get_vendor_interface' function from the vendor library.
  // This function will return a populated vendor_interface_t structure.
  vendor_interface_t (*get_vendor_interface_fn)(void) = 
      (vendor_interface_t (*)(void))dlsym(vendor_lib_handle, "get_vendor_interface");
  if (!get_vendor_interface_fn) 
  {
    fprintf(stderr, "Error getting symbol: %s\n", dlerror());
    dlclose(vendor_lib_handle);
    return -1;
  }

  // Get the vendor interface and store it in the global variable.
  vendor = get_vendor_interface_fn();

  // Initialize the vendor library using the loaded function.
  if (vendor.vendor_init() != 0) 
  {
    fprintf(stderr, "Error initializing vendor library\n");
    dlclose(vendor_lib_handle);
    return -1;
  }

  return 0;
}

// HAL function implementations using the vendor interface.
// These functions call the corresponding functions in the 
// dynamically loaded vendor library.
int hal_read_sensor(int sensor_id) 
{
  return vendor.vendor_read_sensor(sensor_id);
}

void hal_write_actuator(int actuator_id, int value) 
{
  vendor.vendor_write_actuator(actuator_id, value);
}

// --- vendor_lib_a.c (Example vendor implementation) ---

#include "hal_interface.h"

// Vendor A specific implementations of the functions defined in vendor_interface_t
int vendor_init(void) 
{
  // ... vendor A initialization ...
  printf("Vendor A initialized\n");
  return 0;
}

int vendor_read_sensor(int sensor_id) 
{
  // ... vendor A sensor reading ...
  printf("Vendor A reading sensor %d\n", sensor_id);
  return sensor_id * 10;
}

void vendor_write_actuator(int actuator_id, int value) 
{
  // ... vendor A actuator writing ...
  printf("Vendor A writing %d to actuator %d\n", value, actuator_id);
}

// Export the 'get_vendor_interface' function. This function returns 
// a vendor_interface_t structure populated with the vendor's function pointers.
vendor_interface_t get_vendor_interface(void) 
{
  static vendor_interface_t vendor = 
  {
    .vendor_init = vendor_init,
    .vendor_read_sensor = vendor_read_sensor,
    .vendor_write_actuator = vendor_write_actuator
  };
  return vendor;
}

// --- main.c (Example application) ---

#include "hal_interface.h"
#include <stdio.h>

int main() 
{
  // Initialize the HAL. This will load the vendor library and initialize
  // the vendor-specific hardware.
  if (hal_init() != 0) 
  {
    fprintf(stderr, "HAL initialization failed\n");
    return 1;
  }

  // Use the HAL functions to interact with the hardware.
  printf("Sensor value: %d\n", hal_read_sensor(5));
  hal_write_actuator(2, 100);

  return 0;
}

Code Structure

  • hal_interface.h: Defines the vendor and HAL interfaces.

    • vendor_interface_t: Structure holding function pointers for vendor-specific functions.
    • HAL functions: Concrete functions providing a consistent interface to the application.
  • hal_impl.c: Implements the HAL and dynamically loads the vendor library.

    • load_dependency(): Loads a shared library at runtime using dlopen.
    • hal_init(): Initialises the HAL, loads the vendor library, and retrieves the vendor interface.
    • HAL functions: Implementations using the loaded vendor functions.
  • vendor_lib_a.c: Example vendor library implementation.

    • Vendor functions: Implementations of the functions defined in vendor_interface_t.
    • get_vendor_interface(): Returns a populated vendor_interface_t structure.
  • main.c: Example application using the HAL.

    • hal_init(): Initialises the HAL.
    • HAL functions: Calls to interact with the hardware through the HAL.

Benefits

  • Abstraction: Provides a consistent HAL interface for different vendors.
  • Modularity: Isolates vendor-specific code.
  • Flexibility: Enables easy switching between vendors.

Example Output:

Vendor A initialised
Vendor A reading sensor 5
Vendor A writing 100 to actuator 2
Sensor value: 50

FAQ: What happens if the library that's being loaded has dependancies

When you use dlopen to load a shared library, the dynamic linker also takes care of loading any other shared libraries that the loaded library depends on. This process is called dependency resolution.

Here's how it works:

  1. Dependency List: Every shared library contains a list of its dependencies (other shared libraries it needs to function). This information is stored within the library file itself.

  2. Recursive Loading: When you load a library with dlopen, the dynamic linker examines its dependency list. For each dependency:

    • It checks if the dependency is already loaded. If it is, it's reused.
    • If not, it recursively loads the dependency using the same dlopen process, which may in turn load further dependencies.
  3. Symbol Resolution: Once all dependencies are loaded, the dynamic linker resolves symbols (function names, variables) between the libraries. This ensures that calls from one library to another are correctly linked.

  4. Search Path: The dynamic linker uses a search path to locate dependencies. This path is typically defined by the LD_LIBRARY_PATH environment variable and/or the rpath (runtime path) embedded in the library.

Example:

If libA.so depends on libB.so and libC.so, and you call dlopen("libA.so", ...), the following will happen:

  1. libA.so is loaded.
  2. The dynamic linker sees that libA.so needs libB.so and libC.so.
  3. libB.so and libC.so are loaded (if they weren't already).
  4. Symbols between libA.so, libB.so, and libC.so are resolved.

Important Considerations:

  • Circular Dependencies: If you have circular dependencies (e.g., libA.so depends on libB.so, and libB.so depends on libA.so), the dynamic linker may not be able to resolve them, leading to errors.
  • Versioning: Different versions of a library might exist. The dynamic linker needs to ensure that the correct versions are loaded to avoid compatibility issues. This is usually handled through sonames (e.g., libmylib.so.1).
  • Error Handling: If a dependency cannot be found or loaded, dlopen will fail, and you should handle the error appropriately.

In summary, dlopen not only loads the specified library but also automatically handles the loading and linking of its dependencies, ensuring that the library can function correctly. This makes it easier to manage complex applications with multiple shared libraries.

Clone this wiki locally