DLLs Explained: Understanding Dynamic Link Libraries in Windows Client

Table of Contents

Dynamic Link Libraries (DLLs) are fundamental components of the Windows operating system, offering a powerful mechanism for code reuse and efficient resource management. This article delves into the intricacies of DLLs, covering their core concepts, potential issues, and advanced development considerations for Windows client environments. Understanding DLLs is crucial for both system administrators troubleshooting applications and developers building robust Windows software.

Dynamic Link Libraries Windows

A Dynamic Link Library (DLL) is a type of executable file that acts as a shared library of functions and resources for multiple applications. Unlike static libraries, which are linked into an executable at compile time, DLLs are loaded into memory and linked at runtime. This dynamic approach offers significant advantages in terms of memory efficiency, modularity, and ease of updates. Much of the core functionality of the Windows operating system itself is provided through a vast network of DLLs.

When a program runs on Windows, many of its features are often sourced from these shared libraries. For instance, a program might utilize a DLL to implement common user interface elements like “Open” or “Save” dialog boxes, rather than embedding that code directly. This design philosophy underpins the efficiency and flexibility of the Windows application ecosystem.

Key Concepts and Benefits of DLLs

The use of DLLs inherently promotes several beneficial software development and deployment practices. These include modularization of code, extensive code reuse across different applications, efficient utilization of system memory, and a reduction in overall disk space consumption. Consequently, applications and the operating system itself can load and execute faster, leading to a more responsive computing experience.

However, the shared nature of DLLs also introduces potential challenges, primarily concerning dependencies. When an application relies on a specific version of a DLL, a “dependency” is established. If another program inadvertently overwrites or alters this shared DLL in a way that breaks compatibility, the original application may fail to launch or function correctly. This phenomenon, often referred to as “DLL Hell,” highlights the need for careful management of these shared components.

At its core, a DLL is a collection of compiled code and data that can be simultaneously accessed by one or more programs. For example, the Comdlg32.dll in Windows operating systems provides a suite of functions for common dialog boxes. Any program can utilize the functions within this single DLL to display an Open dialog box, thereby eliminating the need for each program to implement this functionality independently. This mechanism significantly fosters code reuse and optimizes memory usage by loading shared code only once into memory.

Furthermore, DLLs enable a program to be designed with a modular architecture, breaking it down into distinct, manageable components. Consider an accounting application: it might be sold with various modules, such as payroll, accounts receivable, and general ledger. Each module can be implemented as a separate DLL and loaded into the main program only when its specific functionality is required at runtime. This approach reduces the initial load time of the main application and conserves memory by loading modules on demand.

The modularity also simplifies the process of updating software. If tax rates change annually, for instance, only the payroll module’s DLL needs to be updated and replaced, rather than recompiling and redeploying the entire accounting application. This targeted update mechanism minimizes disruption and streamlines maintenance efforts.

Common Types of DLL Files in Windows

In the Windows operating systems, several types of files are inherently implemented as DLLs, showcasing their widespread application:

  • ActiveX Controls (.ocx) files: These are self-contained software components that can be reused in various applications. A classic example is a calendar control, which allows users to select dates from a graphical calendar interface. These .ocx files are essentially specialized DLLs providing graphical and interactive elements.
  • Control Panel (.cpl) files: Each item you see in the Windows Control Panel, such as “Mouse” or “Network and Sharing Center,” is typically a dedicated .cpl file. These files are specialized DLLs that provide the user interface and logic for configuring specific system settings.
  • Device Driver (.drv) files: Device drivers are critical software components that allow the operating system to communicate with hardware devices. A printer driver, for instance, is a .drv file (which is a form of DLL) that translates commands from applications into instructions a printer can understand, enabling the system to print documents.

DLL architecture diagram

Advantages of Using DLLs

The architectural decision to leverage DLLs offers a multitude of benefits for software developers, system administrators, and end-users alike. These advantages significantly contribute to the overall efficiency, maintainability, and performance of Windows applications.

Efficient Resource Utilization

One of the primary advantages of DLLs is their ability to conserve system resources. When multiple programs on a computer require the same set of functions, a DLL allows that shared code to be loaded into physical memory only once, rather than having each application load its own copy. This mechanism dramatically reduces the duplication of code on the disk and, more critically, in RAM. The outcome is improved performance not only for the active foreground application but also for other programs running concurrently on the Windows operating system, as less memory contention occurs.

Promotion of Modular Architecture

DLLs are instrumental in encouraging and facilitating modular program design. This approach is particularly valuable for developing large, complex applications that might require multiple language versions or extensive feature sets. By segmenting an application into distinct DLLs, development teams can work on different modules independently, improving team collaboration and project management. As seen with the accounting program example, a modular design allows for features to be loaded dynamically as needed, preventing the main application from becoming bloated and slow.

Simplified Deployment and Maintenance

The update and maintenance process for software becomes considerably easier when DLLs are utilized. If a specific function within a DLL requires an update or a bug fix, only that particular DLL needs to be redeployed. The entire application does not need to be recompiled or re-linked with the updated DLL. Furthermore, if several applications share the same DLL, all these applications automatically benefit from the update or fix simultaneously, without individual patching. This benefit is particularly pronounced when integrating third-party DLLs that are frequently updated or patched by their vendors.

Understanding DLL Dependencies and Conflicts

While offering numerous advantages, the shared nature of DLLs also introduces a critical challenge known as “DLL dependency.” A dependency is formed whenever a program or one DLL relies on a function provided by another DLL. This interdependency means the program is no longer entirely self-contained; its ability to run successfully becomes contingent on the presence and compatibility of its dependent DLLs.

If this dependency is broken, the program may encounter severe issues or fail to launch altogether. Common scenarios that lead to broken dependencies include:
* A dependent DLL being upgraded to a new, incompatible version.
* A critical fix to a dependent DLL inadvertently introducing new bugs or breaking existing functionality.
* A newer application overwriting a shared DLL with an older, incompatible version, a common source of “DLL Hell.”
* A dependent DLL being accidentally or intentionally removed from the computer, rendering applications that rely on it non-functional.

These situations are collectively known as DLL conflicts. If backward compatibility is not rigorously maintained by the DLL provider, applications can cease to function, leading to significant user frustration and support challenges.

Mitigating Dependency Issues

To address the prevalent issues of DLL conflicts, significant improvements have been introduced starting with Windows 2000 and continuing in later Windows operating systems. These mechanisms aim to provide greater stability and control over shared system components.

  • Windows File Protection (WFP): Implemented in Windows 2000, WFP is a core operating system feature designed to safeguard critical system DLLs. It prevents unauthorized agents, such as rogue application installers or malicious software, from updating, deleting, or replacing system DLLs with older or non-Microsoft versions. When an installation attempts to modify a protected system DLL, WFP checks for a valid digital signature. If the signature is absent or invalid, WFP silently restores the original, protected version of the file from a cache, ensuring system integrity and stability.

  • Private DLLs: Private DLLs offer a robust solution for isolating applications from changes made to shared DLLs. This mechanism allows an application to use its own specific version of a DLL, regardless of other versions installed on the system. There are two primary ways to implement private DLLs:

    • Version-Specific Information: For newly developed applications, version-specific information can be embedded within the DLL and the application’s manifest. This tells the operating system to load the specific version of the DLL found in the application’s root directory or a designated subfolder, rather than searching the system paths.
    • .local Files: For older applications, an empty file named application_name.exe.local placed in the application’s root folder can achieve a similar effect. The presence of this .local file signals the operating system to first search the application’s directory for DLLs before looking in standard system paths, effectively “localizing” the DLL search.

DLL Hell dependency issues

Essential DLL Troubleshooting Tools

When encountering problems related to DLLs, several specialized tools are available to help diagnose and resolve issues. These tools provide valuable insights into the dependencies, validity, and interactions of DLLs on a system.

Dependency Walker (depends.exe)

The Dependency Walker tool is an indispensable utility for recursively scanning and analyzing all dependent DLLs used by a program or another DLL. When you open an executable or DLL in Dependency Walker, it performs a series of comprehensive checks:
* Missing DLLs: It identifies any DLLs that are declared as dependencies but cannot be found on the system.
* Invalid Files: It checks for program files or DLLs that are corrupt or not valid for the current operating system architecture.
* Function Mismatches: It verifies that import functions (functions an application expects to call from a DLL) correctly match export functions (functions a DLL makes available).
* Circular Dependencies: It detects problematic circular dependency errors, where two or more DLLs are interdependent in a loop.
* Platform Mismatches: It identifies modules that are invalid because they were compiled for a different operating system or CPU architecture (e.g., 32-bit DLL trying to load into a 64-bit process without proper handling).

By using Dependency Walker, developers and IT professionals can meticulously document all DLLs an application relies upon, which is crucial for preventing and correcting future DLL-related problems. This tool is often found in the drive:\Program Files\Microsoft Visual Studio\Common\Tools directory when Visual Studio 6.0 is installed, and newer versions can be found online.

DLL Universal Problem Solver (DUPS)

The DLL Universal Problem Solver (DUPS) is a suite of utilities designed for auditing, comparing, documenting, and displaying detailed information about DLLs on a system. It provides a more programmatic and systematic approach to managing DLL information.

The DUPS tool comprises the following utilities:
* Dlister.exe: This command-line utility enumerates all DLLs present on a computer, capturing their details (version, path, size, date) and logging this information into a structured text file or directly into a database file for further analysis.
* Dcomp.exe: Used in conjunction with Dlister.exe, this utility compares the DLL information contained in two different text files generated by Dlister.exe. It then produces a third text file that precisely outlines the differences between the two sets of DLLs, highlighting changes, additions, or deletions.
* Dtxt2DB.exe: This utility is designed to import the text files created by Dlister.exe and Dcomp.exe into a dllHell database. This database acts as a centralized repository for tracking DLL versions and changes over time, aiding in long-term dependency management.
* DlgDtxt2DB.exe: Providing a user-friendly graphical interface, DlgDtxt2DB.exe offers an intuitive way to interact with the functionalities of the Dtxt2DB.exe utility, making the process of loading and managing DLL data in the database more accessible.

DLL Help Database

The DLL Help database serves as a valuable resource for identifying specific versions of DLLs that are installed by various Microsoft software products. This database helps users and administrators determine the correct version of a DLL associated with a particular Microsoft application or component, which can be crucial when troubleshooting compatibility or installation issues.

Dependency Walker tool

Developing Your Own DLLs

Developing custom Dynamic Link Libraries requires careful consideration of several technical aspects, including how applications will link to your DLL and how functions will be exposed. This section outlines the primary methods for loading DLLs and essential development practices.

Types of Dynamic Linking

When an application needs to utilize functions within a DLL, there are two fundamental methods of dynamic linking: load-time dynamic linking and run-time dynamic linking. The choice between these methods depends on the application’s specific requirements, such as startup performance and flexibility.

Load-Time Dynamic Linking

In load-time dynamic linking, an application makes explicit calls to exported DLL functions as if they were local functions within the application itself. To achieve this, the application must be compiled and linked with two specific files provided by the DLL: a header (.h) file and an import library (.lib) file.
* The header file contains the function declarations, allowing the compiler to perform type checking.
* The import library contains information that the linker uses to resolve the DLL function’s location at load time. This means that when the application starts, the operating system ensures the required DLLs are loaded into memory and the addresses of their exported functions are made available to the application before execution begins.

This method is simpler to implement from a coding perspective, as function calls resemble regular local function calls. However, it means the application has a hard dependency on the DLL at startup; if the DLL is missing or corrupted, the application will fail to launch.

Run-Time Dynamic Linking

Run-time dynamic linking offers greater flexibility by allowing an application to load a DLL and obtain addresses of its functions during its execution, rather than at startup. To use this method, an application calls the LoadLibrary or LoadLibraryEx function to load the DLL into its address space at a specific point in its lifecycle. After successful loading, the GetProcAddress function is then used to retrieve the memory address of a specific exported DLL function.

The primary advantage of run-time dynamic linking is that it does not require an import library file during compilation. This method makes the application less dependent on the DLL at startup, allowing it to handle situations where the DLL might be optional or unavailable.

Application Criteria for Linking Methods

The choice between load-time and run-time dynamic linking depends on specific application needs:

  • Startup Performance: If the initial startup performance of the application is a critical factor, run-time dynamic linking can be advantageous. It allows the application to defer loading non-essential DLLs until they are actually needed, reducing the initial memory footprint and launch time.
  • Ease of Use: Load-time dynamic linking generally provides a simpler development experience. Exported DLL functions are called like any other local function, simplifying the code and reducing the complexity of error handling related to DLL loading.
  • Application Logic and Flexibility: Run-time dynamic linking is superior for applications that need to dynamically load different modules or versions of DLLs based on specific conditions, user choices, or system capabilities. This flexibility is particularly useful for developing multilingual applications where different language resource DLLs can be loaded as required.

LoadLibrary GetProcAddress

The DLL Entry Point (DllMain)

When you create a DLL, you have the option to specify an entry point function, traditionally named DllMain. This function is invoked by the operating system under various circumstances related to the DLL’s lifecycle. Specifically, DllMain is called when processes or threads attach to the DLL (load it) or detach from it (unload it).

The DllMain function serves as an ideal place for performing essential initialization or cleanup tasks for the DLL. For instance, you can initialize global data structures when a process attaches, or deallocate resources when a process detaches. For multithreaded applications, DllMain can also be used with thread local storage (TLS) to allocate memory that is private to each thread, preventing data corruption in shared global variables.

An example of the DllMain function signature and its typical structure is as follows:

BOOL APIENTRY DllMain(
HANDLE hModule, // Handle to DLL module
DWORD ul_reason_for_call, // Reason for calling function
LPVOID lpReserved ) // Reserved
{
    switch ( ul_reason_for_call )
    {
        case DLL_PROCESS_ATTACHED: // A process is loading the DLL.
        break;
        case DLL_THREAD_ATTACHED: // A process is creating a new thread.
        break;
        case DLL_THREAD_DETACH: // A thread exits normally.
        break;
        case DLL_PROCESS_DETACH: // A process unloads the DLL.
        break;
    }
    return TRUE;
}

The ul_reason_for_call parameter indicates the specific event triggering the DllMain call:
* DLL_PROCESS_ATTACHED: A new process is loading the DLL. This is where process-wide initialization should occur.
* DLL_THREAD_ATTACHED: A new thread is being created within a process that has the DLL loaded. This can be used for thread-specific initialization.
* DLL_THREAD_DETACH: A thread is exiting normally from a process that has the DLL loaded. This is for thread-specific cleanup.
* DLL_PROCESS_DETACH: A process is unloading the DLL, either because it’s terminating or explicitly calling FreeLibrary. This is for process-wide cleanup.

It is crucial that the DllMain function executes quickly and performs only simple initialization tasks. It should not perform complex operations that might lead to deadlocks or other issues, such as directly or indirectly calling LoadLibrary or FreeLibrary functions, especially during process termination. Returning FALSE from DllMain during DLL_PROCESS_ATTACHED will prevent the application from starting if using load-time dynamic linking, or prevent the individual DLL from loading if using run-time dynamic linking.

Note: In multithreaded applications, strong attention must be paid to synchronizing access to any DLL global data. Using mechanisms like thread local storage (TLS), mutexes, or critical sections is essential to ensure thread safety and prevent potential data corruption.

Exporting Functions from a DLL

To make functions available for use by other applications or DLLs, they must be explicitly exported from the DLL. There are two primary methods for exporting DLL functions: using function keywords or creating a module definition (.def) file.

Using Function Keywords

The most common method involves decorating the function declaration with a specific keyword. For functions you intend to export from a DLL, you use:
__declspec(dllexport)

Conversely, when an application or another DLL wants to import and use these exported functions, it typically declares them with:
__declspec(dllimport)

Often, developers will use a single header file for both export and import scenarios by leveraging a preprocessor define and an ifdef statement. This allows the same header file to be included by both the DLL’s source code (where functions are exported) and the client application’s source code (where functions are imported).

// SampleDLL.h
#ifndef INDLL_H
#define INDLL_H

#ifdef EXPORTING_DLL
    // When compiling the DLL, export the function
    extern __declspec(dllexport) void HelloWorld();
#else
    // When compiling a client app, import the function
    extern __declspec(dllimport) void HelloWorld();
#endif

#endif // INDLL_H

Using Module Definition (.def) Files

An alternative to using function keywords is to create a module definition (.def) file. This text file explicitly lists the functions that the DLL will export. When using a .def file, you do not need to add the __declspec(dllexport) keyword to the function declarations in your source code.

A .def file typically includes the LIBRARY statement, which specifies the name of the DLL, and the EXPORTS statement, which lists the names of the functions to be exported, optionally with an ordinal number.

// SampleDLL.def
//
LIBRARY "sampleDLL"
EXPORTS
    HelloWorld
    AnotherExportedFunction @2

During the linking phase of the DLL compilation, the .def file is processed to generate the import library (.lib) and the actual DLL.

Sample DLL and Application Walkthrough

Let’s illustrate DLL creation and usage with a simple example using Visual C++. In Visual C++, you can start a DLL project by selecting project types such as Win32 Dynamic-Link Library or MFC AppWizard (dll).

Here’s a basic example of a DLL created with the Win32 Dynamic-Link Library project type:

// SampleDLL.cpp
//
#include "stdafx.h"
#define EXPORTING_DLL // Define this when building the DLL
#include "sampleDLL.h" // Includes the header with __declspec magic

BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved )
{
    // Simple DllMain, return TRUE for success
    return TRUE;
}

// Function to be exported
void HelloWorld()
{
    MessageBox( NULL, TEXT("Hello World from a DLL!"), TEXT("In a DLL"), MB_OK);
}

Now, a separate Win32 Application project that calls the HelloWorld function from SampleDLL.dll would look like this:

// SampleApp.cpp
//
#include "stdafx.h"
#include "sampleDLL.h" // Includes the same header, but EXPORTING_DLL is not defined, so it uses __declspec(dllimport)

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    HelloWorld(); // Call the exported function
    return 0;
}

Note: For load-time dynamic linking, when compiling and linking SampleApp, you must link against the SampleDLL.lib import library that was generated when you built the SampleDLL project. This import library tells the linker where to find the HelloWorld function’s entry point in SampleDLL.dll.

For run-time dynamic linking, the client application’s code would be structured differently:

// ... (inside a function or WinMain)
typedef VOID (*DLLPROC) (LPCTSTR); // Define a function pointer type matching HelloWorld's signature
// LPCTSTR is used as MessageBox takes a const TCHAR* which can be LPCTSTR

HINSTANCE hinstDLL; // Handle to the loaded DLL module
DLLPROC HelloWorldFunc; // Function pointer to the exported HelloWorld function
BOOL fFreeDLL;

// Load the DLL at runtime
hinstDLL = LoadLibrary(TEXT("sampleDLL.dll"));
if (hinstDLL != NULL)
{
    // Get the address of the exported function
    // Ensure the function name matches the export (case-sensitive on some systems, though generally not for C++)
    HelloWorldFunc = (DLLPROC) GetProcAddress(hinstDLL, "HelloWorld");
    if (HelloWorldFunc != NULL)
    {
        // Call the function via the function pointer
        HelloWorldFunc(TEXT("Message from SampleApp")); // Pass a string if HelloWorld accepted one, adjusted example for brevity
    }
    // Unload the DLL when no longer needed
    fFreeDLL = FreeLibrary(hinstDLL);
    if (!fFreeDLL)
    {
        // Handle error in FreeLibrary
    }
}
else
{
    // Handle error if DLL could not be loaded
    MessageBox(NULL, TEXT("Failed to load SampleDLL.dll"), TEXT("Error"), MB_OK | MB_ICONERROR);
}
// ...

DLL Search Order

When an application loads a DLL (either implicitly at load time or explicitly via LoadLibrary/LoadLibraryEx), the Windows operating system follows a specific search order to locate the required DLL file. Understanding this order is crucial for correct deployment and troubleshooting:

  1. The application folder: The directory from which the application executable was loaded. This is the first place Windows looks, supporting private DLLs.
  2. The current folder: The current working directory of the process.
  3. The Windows system folder: This directory contains many critical system DLLs. Its path can be retrieved using the GetSystemDirectory function (e.g., C:\Windows\System32).
  4. The Windows folder: The main Windows installation directory. Its path can be retrieved using the GetWindowsDirectory function (e.g., C:\Windows).
  5. The directories listed in the PATH environment variable: Windows searches through all directories specified in the system’s PATH environment variable, in the order they are listed.

This search order can be influenced by specific flags in LoadLibraryEx or by manifest files for side-by-side assembly deployments.

The .NET Framework Assembly: An Evolution

With the advent of the .NET Framework, Microsoft introduced a new concept to address many of the issues traditionally associated with Win32 DLLs: the assembly. While an assembly physically exists as a .dll or .exe file, its internal structure and management by the .NET Common Language Runtime (CLR) are fundamentally different from traditional Win32 DLLs. Assemblies were designed to mitigate “DLL Hell” and provide a more robust, self-describing, and isolated deployment model.

An assembly is a logical unit of functionality that runs under the strict control of the .NET CLR. Each assembly file contains several key components that make it self-describing:

  • Assembly Manifest: This critical part contains comprehensive metadata about the assembly itself. It includes the assembly’s name, version information, culture (localization) information, strong name (for globally unique identification), a list of all files that make up the assembly, type reference information, and details about all referenced and dependent assemblies.
  • Type Metadata: Describes the types (classes, interfaces, structs, enums) defined within the assembly.
  • Microsoft Intermediate Language (MSIL) Code: This is the platform-agnostic, compiled code that the CLR executes after just-in-time (JIT) compilation.
  • Other Resources: Embedded resources such as images, string tables, and user interface definitions.

By default, an assembly is private to the application that uses it, deployed directly within the application’s installation directory. To create a shared assembly that can be used by multiple applications, it must be assigned a strong name (a cryptographically strong unique identity) and then published to the Global Assembly Cache (GAC). The CLR manages the loading and execution of assemblies, ensuring type safety, security, and version integrity.

Assemblies vs. Win32 DLLs: A Comparison

The .NET Framework assemblies offer significant improvements over traditional Win32 DLLs, addressing many long-standing challenges:

  • Self-Describing: Unlike Win32 DLLs, which often require external registration (e.g., COM DLLs) or manifest files to describe their dependencies, an assembly is entirely self-describing. The assembly manifest contains all the necessary information for the CLR to locate, load, and run the assembly, including a definitive list of its dependent assemblies. This ensures that the CLR can maintain a consistent and complete set of assemblies for an application, greatly reducing dependency issues.
  • Versioning: Versioning is robustly enforced by the CLR through the assembly manifest. Each assembly carries explicit version information, and the CLR strictly adheres to this, preventing accidental loading of incompatible versions. Furthermore, version policies allow administrators to define how different versions of an assembly are resolved. In contrast, Win32 DLLs rely on file system timestamps or rudimentary internal version numbers, which the operating system does not actively enforce, leading to frequent compatibility problems.
  • Side-by-Side Deployment: Assemblies inherently support side-by-side deployment, meaning different applications can simultaneously use different versions of the same assembly on the same machine without conflicts. This eliminates many “DLL Hell” scenarios. While Windows 2000 and later introduced private DLLs and manifests to enable some side-by-side functionality for Win32 DLLs, assemblies provide a more comprehensive and robust solution managed directly by the CLR.
  • Self-Containment and Isolation: Applications built with assemblies are generally more self-contained and isolated from other applications running on the computer. This characteristic facilitates “zero-impact” installations and uninstallation processes, as an application’s components are typically localized to its directory and do not interfere with system-wide shared components unless explicitly registered in the GAC.
  • Execution and Security: MSIL code within an assembly is not directly executable but is managed by the CLR. This management includes enforcing security permissions specified in the assembly manifest and controlled by the CLR’s security policies. This provides a sandboxed environment, enhancing the security of applications.
  • Language Independence: One of the powerful features of .NET assemblies is their language independence. An assembly can be developed using any .NET-compliant language (e.g., C#, Visual Basic .NET, F#). Once compiled to MSIL, this assembly can then be seamlessly used by applications written in any other .NET language, fostering interoperability across different programming paradigms within the .NET ecosystem.

.NET assembly manifest

In summary, while traditional Win32 DLLs remain a cornerstone of the Windows operating system and native applications, .NET assemblies represent a significant advancement, offering a more managed, secure, and resilient approach to shared code components in modern software development.


We hope this comprehensive overview has deepened your understanding of Dynamic Link Libraries in Windows. DLLs are a critical part of how Windows and its applications function, offering both immense power and unique challenges.

Do you have any personal experiences with “DLL Hell” or a favorite troubleshooting technique? Share your thoughts and questions in the comments below! We’d love to hear from you and foster a deeper discussion on this fundamental topic.

Post a Comment