Using an opaque pointer to hide struct details when developing a function library in C Language
1. modular design requires the library interface to hide implementation details. As a function library, it is the most basic design standard to minimize coupling with its caller. C language, as the practitioner of the classic "program = Data Structure + algorithm", there must be a large number of struct definitions when implementing the function library, and interface functions need to operate these structs. At the same time, the modularization of the program design requires that the library interface expose its implementation details as little as possible. The interface parameters should use basic data types as much as possible, and avoid exposing the struct definitions in the library in the form parameters. 2. The two methods to hide struct are based on my understanding. There are two most common methods to hide struct definitions in the Library: interface function parameters use struct pointers, handle Used for interface function parameters. 2.1 reference a struct using a struct pointer for convenience. First, an example code written using VC ++ is provided. Library interface header file MySDK. h
# Pragma once # ifdef MYSDK_EXPORT # define MYSDK_API _ declspec (dllexport) # else # define MYSDK_API _ declspec (dllimport) # endiftypedef struct _ Window; /* pre-declare */# ifdef _ cplusplusextern "C" {# endif MYSDK_API Window * CreateWindow (); MYSDK_API void ShowWindow (Window * pWin ); # ifdef _ cplusplus} # endif
Library implementation file MySDK. c
#define MYSDK_EXPORT#include "MySDK.h"#include <stdlib.h>struct _Window{ int width; int height; int x; int y; unsigned char color[3]; int isShow;};MYSDK_API Window* CreateWindow(){ Window* p = malloc(sizeof(Window)); if (p) { p->width = 400; p->height = 300; p->x = 0; p->y = 0; p->color[0] = 255; p->color[1] = 255; p->color[2] = 255; p->isShow = 0; } return p;}MYSDK_API void ShowWindow(Window* pWin){ pWin->isShow = 1;}
Library User code
#include <stdio.h>#include "../myDll/MySDK.h"#pragma comment(lib, "../Debug/myDll.lib")int main(int argc, char** argv){ Window* pWin = CreateWindow(); ShowWindow(pWin); return 0;}
Here, MySDK. h and MySDK. c are the implementation of the library; main. cpp is the implementation of the caller program. Both parties use the same interface header file MySDK. h. But from the user's point of view, main. cpp only knows that there is a struct type named Window in the library, but does not know the implementation details (definitions) of this mechanism ). Since the C/C ++ compiler is a latency-dependent compiler, as long as the source code does not involve the memory layout of the Window struct, you do not need to know the complete definition of the Window during compilation, however, you can still check the correctness of the type name. For example, if the client code is as follows, the compiler will check the problem:
int* p = 0; ShowWindow(p);
Although the compiler does not know the implementation details of the struct pointed to by pWin in ShowWindow (pWin), it can still ensure that the real parameter type is Window *, which facilitates the caller to check for errors. 2.2 The "handle" (handle) is used to reference the concept that the struct first contacts the handle. It is in Win32API. It can be concluded that a large number of structs, such as thread objects, process objects, window objects, and so on, are defined in the Windows system ,..... However, the programming interface Win32API rarely provides the definitions of these struct. The caller indirectly references the struct object to be used through a value called "handle. For example, the following Win32API
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr); ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd);
The window type is a very complex struct in Windows. to hide its implementation details, Microsoft uses the window handle concept to indirectly reference the struct object. To implement this correspondence, the database must maintain the correspondence between the handle and the struct object. The concept of handle in Linux APIS is also widely used in Linux platform APIs. For example
int open(const char *pathname, int flags); ssize_t read(int fd, void *buf, size_t count);
In Linux, a file must be represented by a complex struct, but a simple integer is used in the API to reference it, avoiding exposing the details of the file struct to the caller. The handle in OpenGL API is also applied to the OpenGL library. For example
void WINAPI glGenTextures( GLsizei n, GLuint *textures);void WINAPI glBindTexture( GLenum target, GLuint texture);
Texture is also a complex struct In the OpenGL library, and the Implementation Details are hidden externally using the handle concept. 3. Comparison of handles and pointers 3.1 the advantages and disadvantages of handles look really good. How do I map the local data to the corresponding struct? The easiest way to think of it is to directly use the memory address of the struct object as the handle. However, most library implementations do not. I personally think there are several reasons why I do not directly use the memory address as the handle value: From the Perspective of source code protection, the memory address is more prone to Hack. By knowing the memory address of the struct, you can read the content of the memory, which provides convenience for guessing the details of the struct. From the perspective of program stability, the caller should only access objects maintained inside the database through interface functions. If the caller obtains the memory address of the object, therefore, it is possible to directly modify the database intentionally or unintentionally, thus affecting the stable operation of the database. From the perspective of portability, the pointer type has different lengths in 32-bit and 64-bit systems. Therefore, we need to define two interface functions with duplicate names, resulting in various inconveniences. For example, if OpenGL uses the int type as the handle type, an interface function can span multiple platforms. From the perspective of simplified interface header files, you must declare at least the struct type in advance, such as struct Window, to use the basic data type as the handle. Handle limitations: the compiler cannot identify the specific struct type. Because the data type of the handle is actually the basic data type, the compiler can only perform regular checks and cannot identify the specific struct type. For example
SECURITY_ATTRIBUTES sa; HANDLE h = CreateMutex(&sa, TRUE, L"Mutex"); ReadFile(h, NULL, 0, 0, 0);
The above code compiler does not report an error because the mutex object and the file object both use the same handle type. Efficiency may be slightly lower. After all, there is a process of searching for memory pointers Based on the handle value, which may slightly affect the running efficiency. 3.2 pointer advantages and disadvantages in fact, pointer and handle are opposite. The disadvantage of handle is the advantage of pointer, and the advantage of handle is also the disadvantage of pointer. 4. How to design a large cross-platform database and use a handle. For a dedicated small database, use a pointer. As for my current project, it is a small C-Library Project, and the target group of the library is also relatively single, so in line with the principle of simple enough, I chose to use pointers to hide the Implementation Details of struct in the library.