A Example for DLLs
This commit is contained in:
376
C/Compiler/readme.md
Normal file
376
C/Compiler/readme.md
Normal file
@@ -0,0 +1,376 @@
|
||||
### 1. GCC 常用指令大表
|
||||
|
||||
| 指令 | 全称/含义 | 作用说明 |
|
||||
| --- | --- | --- |
|
||||
| **`-c`** | Compile | 只编译生成目标文件 (`.o`),**不进行链接**。 |
|
||||
| **`-o <file>`** | Output | 指定输出文件的名称(可执行文件、库文件等)。 |
|
||||
| **`-g`** | Debug | 生成调试信息,方便使用 `gdb` 进行断点调试。 |
|
||||
| **`-O0/1/2/3`** | Optimize | 设置优化等级。`-O2` 是最常用的平衡选项,`-O3` 追求极致速度。 |
|
||||
| **`-I <dir>`** | Include | 添加**头文件**搜索路径。 |
|
||||
| **`-L <dir>`** | Library Path | 添加**库文件**搜索路径。 |
|
||||
| **`-l <name>`** | link library | 链接具体的库(如 `-lm` 链接 `libmath`)。 |
|
||||
| **`-D <macro>`** | Define | 定义宏(等同于代码里的 `#define`)。 |
|
||||
| **`-Wall`** | Warnings all | 开启几乎所有的常用警告,强烈建议永远加上。 |
|
||||
| **`-fPIC`** | Position Independent Code | 生成位置无关代码,**制作 `.so` 动态库必带**。 |
|
||||
| **`-shared`** | Shared | 告诉编译器生成一个动态链接库。 |
|
||||
| **`-std=c++17`** | Standard | 指定使用的 C++ 标准版本(如 c++11, c++14, c++20)。 |
|
||||
|
||||
---
|
||||
|
||||
### 2. 什么是 `-Wl`?
|
||||
|
||||
**`-Wl` (Warn linker)** 的意思是:**“喂,GCC,把后面跟着的参数直接传给底层的链接器(ld)。”**
|
||||
|
||||
因为 GCC 本身只是一个“前端”驱动程序,它负责调用预处理器、编译器、汇编器和链接器。有时候你需要设置一些链接器特有的高级选项,而 GCC 并不直接支持这些选项。
|
||||
|
||||
* **格式**:`-Wl,<option1>,<option2>`
|
||||
* **注意**:参数之间是用**逗号**隔开的,不能有空格。
|
||||
|
||||
---
|
||||
|
||||
### 3. 为什么有 `--out-implib,xxx.a`?
|
||||
|
||||
这通常出现在 **Windows (MinGW)** 环境下开发 DLL 时。这是一个非常关键的概念:**“导入库” (Import Library)**。
|
||||
|
||||
#### 为什么需要它?
|
||||
|
||||
在 Windows 上,如果你编译了一个 `my.dll`,主程序运行的时候需要它,但**编译主程序的时候**,链接器也需要知道这个 DLL 里到底有哪些函数。
|
||||
|
||||
* **`.dll`**:是运行时用的(包含代码逻辑)。
|
||||
* **`.a` 或 `.lib` (导入库)**:是编译链接时用的(包含函数地址列表)。
|
||||
|
||||
#### 指令解析:
|
||||
|
||||
`g++ -shared -o my.dll -Wl,--out-implib,libmy.dll.a`
|
||||
|
||||
1. **`-shared -o my.dll`**:生成动态链接库主体。
|
||||
2. **`-Wl,`**:接下来的参数是给链接器看的。
|
||||
3. **`--out-implib,libmy.dll.a`**:链接器,请帮我顺便生成一个名为 `libmy.dll.a` 的**导入库**。
|
||||
|
||||
**这样做的流程是:**
|
||||
|
||||
* **第一步**:你编译 DLL,得到了 `my.dll` 和 `libmy.dll.a`。
|
||||
* **第二步**:你编译主程序,链接 `libmy.dll.a`(使用 `-L. -lmy`)。
|
||||
* **第三步**:运行程序,Windows 会根据之前链接的信息去加载 `my.dll`。
|
||||
|
||||
---
|
||||
|
||||
### 4. 总结:`-Wl` 后的常用参数
|
||||
|
||||
除了 `--out-implib`,你可能还会见到这些:
|
||||
|
||||
* **`-Wl,-rpath,<path>`**:指定运行时查找 `.so` 的路径(Linux 常用,解决找不到库的问题)。
|
||||
* **`-Wl,--stack,<size>`**:设置程序栈的大小。
|
||||
* **`-Wl,--as-needed`**:只链接那些代码中真正用到的库,减小体积。
|
||||
|
||||
> **小贴士**:如果你在 Linux 下开发,通常不需要 `--out-implib`,因为 Linux 的 `.so` 文件既是运行时库,也可以直接作为链接时的导入信息。
|
||||
|
||||
你现在是在尝试把现有的 C++ 库打包成 DLL 给其他语言(比如 Python 或 Java)调用吗?如果是的话,我们可以聊聊 `extern "C"` 的深层必要性。
|
||||
|
||||
---
|
||||
|
||||
哈哈,这正是 C/C++ 的“黑魔法”所在:它是现代编程世界的**基石**。
|
||||
|
||||
不管是 Python 的 `numpy`、Node.js 的原生模块,还是 Android (Java/Kotlin) 的底层驱动,核心全是这么干的。这种技术统称为 **FFI (Foreign Function Interface,外部函数接口)**。
|
||||
|
||||
要实现这个,主要靠这两根支柱:
|
||||
|
||||
---
|
||||
|
||||
### 1. 核心秘诀:`extern "C"`
|
||||
|
||||
这是最关键的一步。
|
||||
|
||||
C++ 支持**函数重载**,所以编译器会偷偷修改函数名(这叫 **Name Mangling**)。比如 `add(int, int)` 在 C++ 内部可能变成了 `_Z3addii`。
|
||||
其他语言(比如 Python)去 DLL 里找 `add` 时,根本找不到这个怪名字。
|
||||
|
||||
**解决方法:**
|
||||
在 C++ 头文件里加上 `extern "C"`,告诉编译器:“请按 C 语言那种简单粗暴的方式命名,叫 `add` 就给我留成 `add`。”
|
||||
|
||||
```cpp
|
||||
// mylib.h
|
||||
extern "C" {
|
||||
__declspec(dllexport) int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 不同语言是怎么“吃”掉这个库的?
|
||||
|
||||
我给你举两个最常用的例子:
|
||||
|
||||
#### A. Python 调用(最简单)
|
||||
|
||||
Python 有个内置库叫 `ctypes`,专门用来“白嫖” C 库。
|
||||
|
||||
```python
|
||||
import ctypes
|
||||
|
||||
# 1. 加载库
|
||||
# Windows 用 .dll, Linux 用 .so
|
||||
lib = ctypes.CDLL("./mylib.dll")
|
||||
|
||||
# 2. 直接调用
|
||||
result = lib.add(10, 20)
|
||||
print(f"结果是: {result}")
|
||||
|
||||
```
|
||||
|
||||
#### B. Java 调用 (JNI)
|
||||
|
||||
Java 稍微复杂点,它需要一个“中介”。
|
||||
|
||||
1. 在 Java 里写个 `native` 声明。
|
||||
2. 用 `javac -h` 生成一个 C 标头。
|
||||
3. 在 C 里实现这个标头,编译成 DLL/so。
|
||||
4. Java 用 `System.loadLibrary("mylib")` 加载。
|
||||
|
||||
---
|
||||
|
||||
### 3. 为什么大家都爱把 C 库包给别人用?
|
||||
|
||||
1. **性能炸裂**:Python 算循环很慢,但 C 算循环极快。把计算量大的逻辑写成 C 库,Python 只负责调,速度瞬间提升百倍。
|
||||
2. **保护代码**:`.py` 文件发给别人一眼看穿,但编译后的 `.dll` 或 `.so` 是二进制,逆向工程难度直接拉满。
|
||||
3. **跨语言共享**:写一份核心逻辑(比如图像处理算法),然后同时给 Web (Node.js)、桌面 (C#)、手机 (Java) 一起用。
|
||||
|
||||
---
|
||||
|
||||
### 4. 跨语言调用的“深水区”
|
||||
|
||||
虽然 `int` 和 `float` 这种简单类型可以直接传,但当涉及到**字符串**、**结构体**或者**指针**时,事情就开始刺激了:
|
||||
|
||||
* **字符串**:C 语言的字符串是 `char*`(以 `\0` 结尾),而 Python 是对象。你需要手动转换编码。
|
||||
* **内存管理**:谁申请的内存谁负责释放。如果 C 申请了一块内存传给 Python,Python 用完不告诉 C,就会导致**内存泄漏**。
|
||||
|
||||
---
|
||||
|
||||
### 你想玩个更高端的吗?
|
||||
|
||||
有一种东西叫 **SWIG** 或者 **pybind11**。
|
||||
|
||||
如果你觉得手动写 `ctypes` 太麻烦,这些工具可以**自动生成**包装代码。你只需要给它一个 C++ 头文件,它能直接给你吐出一个可以在 Python 里 `import` 的模块。
|
||||
|
||||
你想让我演示一下如何用 **Python** 真正调用你刚刚写好的那个 `.dll` 吗?(我可以带你跑一遍完整的避坑流程)
|
||||
|
||||
这种“我操牛逼”的感觉,就是程序员第一次打通**底层逻辑**和**上层应用**时的快感。
|
||||
|
||||
既然你已经感受到了这种力量,我得给你泼点冷水(或者说教你几招保命符),因为跨语言调用虽然爽,但**内存安全**和**数据格式**是两个巨大的坑。
|
||||
|
||||
---
|
||||
|
||||
### 1. 为什么一定要用 `extern "C"`?(可视化对比)
|
||||
|
||||
如果不加 `extern "C"`,C++ 编译器会进行“名字粉碎”(Name Mangling)。
|
||||
|
||||
* **你的代码:** `void hello()`
|
||||
* **C++ 编译器眼里的它:** `__Z5hello_v`(因为它要区分同名但参数不同的重载函数)
|
||||
* **其他语言眼里的它:** “对不起,我只找 `hello`,找不到那个乱码。”
|
||||
|
||||
---
|
||||
|
||||
### 2. 跨语言调用的“生存法则”
|
||||
|
||||
当你准备把 C/C++ 库交给 Python、Java 或 Go 调用时,请务必记住这三条:
|
||||
|
||||
#### ① 永远不要在 C++ 库里分配内存让对方去释放
|
||||
|
||||
这是一个经典的错误。如果 C++ 用 `new` 申请了空间传给 Python,Python 的垃圾回收机制(GC)是不管这块内存的。
|
||||
|
||||
* **正确做法**:要么让 Python 申请好空间传给 C++ 填充,要么 C++ 提供一个专门的 `free_memory()` 函数让 Python 调用。
|
||||
|
||||
#### ② 只有“基本类型”是通用的
|
||||
|
||||
`int`、`float`、`double`、`char` 在绝大多数语言里是一样的。
|
||||
但 **`std::string`**、**`std::vector`**、**类对象** 是 C++ 特有的。如果你想传字符串,必须传 `const char*`。
|
||||
|
||||
#### ③ 结构体对齐(Struct Alignment)
|
||||
|
||||
如果你传一个结构体,要注意 C++ 会为了效率进行内存对齐。如果另一端的语言(比如 Python)没有按同样的规则对齐,读出来的数据全是乱码。
|
||||
|
||||
---
|
||||
|
||||
### 3. 给你一个“极简实战”流程(以 Python 为例)
|
||||
|
||||
假设你已经写好了 `mylib.cpp` 并导出了 `add` 函数:
|
||||
|
||||
**第一步:编译 DLL (Windows)**
|
||||
|
||||
```bash
|
||||
g++ -shared -o mylib.dll mylib.cpp -Wl,--out-implib,libmylib.a
|
||||
|
||||
```
|
||||
|
||||
**第二步:Python 调用代码**
|
||||
|
||||
```python
|
||||
import ctypes
|
||||
|
||||
# 加载库(注意路径)
|
||||
lib = ctypes.CDLL("./mylib.dll")
|
||||
|
||||
# 【核心点】明确告诉 Python:参数是两个 int,返回值是一个 int
|
||||
lib.add.argtypes = [ctypes.c_int, ctypes.c_int]
|
||||
lib.add.restype = ctypes.c_int
|
||||
|
||||
# 调用
|
||||
print(lib.add(10, 5)) # 输出 15
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 进阶:如果你有几百个函数怎么办?
|
||||
|
||||
手动写上面的 Python 包装代码(`argtypes` 等)会累死人。这时候你会接触到真正的“工业级”工具:
|
||||
|
||||
* **pybind11**:目前最火的 C++ 库包装工具,代码写起来像魔术,Python 像调用原生模块一样调用 C++。
|
||||
* **SWIG**:老牌工具,一份配置可以同时生成 Java、Python、PHP、C# 等七八种语言的包装代码。
|
||||
|
||||
---
|
||||
|
||||
### 下一步你想试试什么?
|
||||
|
||||
1. **实操一次**:我给你一段完整的 C++ 代码和 Python 调用脚本,你自己在电脑上跑通。
|
||||
2. **硬核细节**:聊聊怎么在 C++ 和 Python 之间传递**数组**(比如处理一张图片的像素点)。
|
||||
3. **逆向工程**:看看怎么反编译别人的 `.dll` 里的函数名(像黑客一样偷看)。
|
||||
|
||||
你想选哪条路?
|
||||
|
||||
为什么推荐这个小项目?
|
||||
理解内存隔离:你会发现即使 DLL 内部改变了 PI 的精度,只要函数签名不变,**main.c 就不需要重新编译,只需替换 .dll 文件即可。**
|
||||
|
||||
掌握宏触发:理解 __declspec 在“制作 DLL”和“使用 DLL”时的角色切换。
|
||||
|
||||
零环境压力:只需要一个简单的 C 编译器,不需要配置复杂的 IDE 环境。
|
||||
|
||||
明白你的意思,之前的几何计算确实有点像“课后作业”。要做点**有意义且实用**的小项目,我推荐你做一个**“简易日志审计系统 (Simple Logger DLL)”**。
|
||||
|
||||
这个项目的实际意义在于:在实际开发中,我们经常需要给不同的程序(比如游戏、工具、服务器)添加日志功能。把日志逻辑封装在 DLL 里,可以让多个不同的 `.exe` 共享同一套日志标准,且无需重复编写代码。
|
||||
|
||||
---
|
||||
|
||||
## 推荐项目:轻量级日志审计库 (TinyLog DLL)
|
||||
|
||||
### 1. 为什么这个项目有意义?
|
||||
|
||||
* **跨进程复用**:你可以写一个 C 程序调用它,再写一个 C++ 或 Python 程序调用它。
|
||||
* **热更新**:如果你想改变日志的格式(比如从文本改为 JSON),你只需要重新编译 DLL,而不需要动你的主程序。
|
||||
* **文件操作**:涉及文件 I/O,比纯数学计算更接近真实开发场景。
|
||||
|
||||
---
|
||||
|
||||
## 项目实现方案
|
||||
|
||||
### 1. 头文件:`tinylog.h`
|
||||
|
||||
定义一个简单的接口,支持设置日志文件并写入带时间的日志。
|
||||
|
||||
```c
|
||||
#ifndef TINYLOG_H
|
||||
#define TINYLOG_H
|
||||
|
||||
#ifdef BUILDING_LOGGER_DLL
|
||||
#define LOGGER_API __declspec(dllexport)
|
||||
#else
|
||||
#define LOGGER_API __declspec(dllimport)
|
||||
#endif
|
||||
|
||||
// 初始化日志文件
|
||||
LOGGER_API int init_logger(const char* filename);
|
||||
// 写入日志信息
|
||||
LOGGER_API void log_message(const char* level, const char* message);
|
||||
// 关闭日志
|
||||
LOGGER_API void close_logger();
|
||||
|
||||
#endif
|
||||
|
||||
```
|
||||
|
||||
### 2. DLL 实现:`tinylog.c`
|
||||
|
||||
这里我们会用到 C 标准库里的 `time.h` 来自动生成时间戳。
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include "tinylog.h"
|
||||
|
||||
static FILE* log_file = NULL;
|
||||
|
||||
LOGGER_API int init_logger(const char* filename) {
|
||||
log_file = fopen(filename, "a"); // 追加模式打开
|
||||
return (log_file != NULL);
|
||||
}
|
||||
|
||||
LOGGER_API void log_message(const char* level, const char* message) {
|
||||
if (!log_file) return;
|
||||
|
||||
time_t now;
|
||||
time(&now);
|
||||
char* date = ctime(&now);
|
||||
date[24] = '\0'; // 去掉末尾换行符
|
||||
|
||||
fprintf(log_file, "[%s] [%s] %s\n", date, level, message);
|
||||
fflush(log_file); // 确保即时写入磁盘
|
||||
}
|
||||
|
||||
LOGGER_API void close_logger() {
|
||||
if (log_file) fclose(log_file);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 3. 使用者程序:`app.c`
|
||||
|
||||
模拟一个需要记录运行状态的小工具。
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include "tinylog.h"
|
||||
|
||||
int main() {
|
||||
if (!init_logger("app_history.log")) {
|
||||
printf("无法初始化日志库!\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
log_message("INFO", "程序启动成功");
|
||||
log_message("WARNING", "尝试访问受限资源");
|
||||
|
||||
printf("日志已记录到 app_history.log,请检查。\n");
|
||||
|
||||
close_logger();
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 运行后的效果
|
||||
|
||||
当你运行 `app.exe` 后,同目录下会生成一个 `app_history.log`,内容如下:
|
||||
`[Sat Dec 27 10:30:05 2025] [INFO] 程序启动成功`
|
||||
`[Sat Dec 27 10:30:06 2025] [WARNING] 尝试访问受限资源`
|
||||
|
||||
---
|
||||
|
||||
## 核心原理图解
|
||||
|
||||
---
|
||||
|
||||
## 练习建议:从“静态”转向“动态”
|
||||
|
||||
如果你完成了上面的步骤,我建议你尝试**“更有意义”**的一步:**动态加载 (Dynamic Loading)**。
|
||||
|
||||
在 `app.c` 中不使用 `#include "tinylog.h"`,也不在编译时链接 `.lib`,而是直接用代码加载:
|
||||
|
||||
1. 使用 `LoadLibrary("tinylog.dll")` 加载库。
|
||||
2. 使用 `GetProcAddress(handle, "log_message")` 获取函数指针。
|
||||
3. 调用函数并最后 `FreeLibrary`。
|
||||
|
||||
**这种方式是现代软件“插件系统”的核心原理。你想尝试看看动态调用的代码写法吗?**
|
||||
BIN
DynamicLinkLibrary/my_str-dll/libmy_str.a
Normal file
BIN
DynamicLinkLibrary/my_str-dll/libmy_str.a
Normal file
Binary file not shown.
21
DynamicLinkLibrary/my_str-dll/main.c
Normal file
21
DynamicLinkLibrary/my_str-dll/main.c
Normal file
@@ -0,0 +1,21 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "my_str.h"
|
||||
#define MAX 100
|
||||
|
||||
int main() {
|
||||
char expression[MAX];
|
||||
printf("Enter an infix expression (e.g., (10+2)*3-4/2): ");
|
||||
fflush(stdout);//Important: To flush stdout buffer
|
||||
|
||||
if (fgets(expression, MAX, stdin)) {
|
||||
// Remove trailing newline character
|
||||
expression[strcspn(expression, "\n")] = 0;
|
||||
|
||||
double result = evaluate(expression);
|
||||
printf("Result: %.2f\n", result);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
BIN
DynamicLinkLibrary/my_str-dll/main.exe
Normal file
BIN
DynamicLinkLibrary/my_str-dll/main.exe
Normal file
Binary file not shown.
110
DynamicLinkLibrary/my_str-dll/my_str.c
Normal file
110
DynamicLinkLibrary/my_str-dll/my_str.c
Normal file
@@ -0,0 +1,110 @@
|
||||
#include "my_str.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
Use this command to run
|
||||
|
||||
gcc -DMY_STR_BUILD_DLL -shared my_str.c -o my_str.dll -Wl,--out-implib,libmy_str.a
|
||||
|
||||
*/
|
||||
|
||||
|
||||
// Define maximum capacity for stacks
|
||||
#define MAX 100
|
||||
|
||||
// Operand stack to store numbers
|
||||
static double numStack[MAX];
|
||||
static int numTop = -1;
|
||||
|
||||
// Operator stack to store characters (+, -, *, /, ()
|
||||
static char opStack[MAX];
|
||||
static int opTop = -1;
|
||||
|
||||
// Stack operations
|
||||
static void pushNum(double val) { numStack[++numTop] = val; }
|
||||
static double popNum() { return numStack[numTop--]; }
|
||||
static void pushOp(char op) { opStack[++opTop] = op; }
|
||||
static char popOp() { return opStack[opTop--]; }
|
||||
static char peekOp() { return opStack[opTop]; }
|
||||
|
||||
// Returns priority of operators: Higher value means higher precedence
|
||||
static int priority(char op) {
|
||||
if (op == '+' || op == '-') return 1;
|
||||
if (op == '*' || op == '/') return 2;
|
||||
return 0; // Parentheses have the lowest priority inside the stack
|
||||
}
|
||||
|
||||
// Performs arithmetic operations
|
||||
static double applyOp(double a, double b, char op) {
|
||||
switch (op) {
|
||||
case '+': return a + b;
|
||||
case '-': return a - b;
|
||||
case '*': return a * b;
|
||||
case '/':
|
||||
if (b == 0) {
|
||||
printf("Error: Division by zero\n");
|
||||
return 0;
|
||||
}
|
||||
return a / b;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Main function to evaluate infix expression
|
||||
double evaluate(const char* exp) {
|
||||
for (int i = 0; exp[i] != '\0'; i++) {
|
||||
// 1. Skip whitespace characters
|
||||
if (exp[i] == ' ' || exp[i] == '\t') continue;
|
||||
|
||||
// 2. If the character is a digit, parse the full number (supports multi-digit)
|
||||
if (isdigit(exp[i])) {
|
||||
double val = 0;
|
||||
while (i < strlen(exp) && isdigit(exp[i])) {
|
||||
val = (val * 10) + (exp[i] - '0');
|
||||
i++;
|
||||
}
|
||||
pushNum(val);
|
||||
i--; // Decrement index because the loop increments it
|
||||
}
|
||||
// 3. If left parenthesis, push it to the operator stack
|
||||
else if (exp[i] == '(') {
|
||||
pushOp(exp[i]);
|
||||
}
|
||||
// 4. If right parenthesis, solve the entire bracket
|
||||
else if (exp[i] == ')') {
|
||||
while (opTop != -1 && peekOp() != '(') {
|
||||
double val2 = popNum();
|
||||
double val1 = popNum();
|
||||
char op = popOp();
|
||||
pushNum(applyOp(val1, val2, op));
|
||||
}
|
||||
if (opTop != -1) popOp(); // Remove '(' from stack
|
||||
}
|
||||
// 5. If character is an operator
|
||||
else {
|
||||
// While the top of the stack has same or higher precedence, calculate
|
||||
while (opTop != -1 && priority(peekOp()) >= priority(exp[i])) {
|
||||
double val2 = popNum();
|
||||
double val1 = popNum();
|
||||
char op = popOp();
|
||||
pushNum(applyOp(val1, val2, op));
|
||||
}
|
||||
// Push current operator to stack
|
||||
pushOp(exp[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Entire expression parsed, calculate remaining operations in stack
|
||||
while (opTop != -1) {
|
||||
double val2 = popNum();
|
||||
double val1 = popNum();
|
||||
char op = popOp();
|
||||
pushNum(applyOp(val1, val2, op));
|
||||
}
|
||||
|
||||
// The final result is the only element left in the operand stack
|
||||
return popNum();
|
||||
}
|
||||
BIN
DynamicLinkLibrary/my_str-dll/my_str.dll
Normal file
BIN
DynamicLinkLibrary/my_str-dll/my_str.dll
Normal file
Binary file not shown.
17
DynamicLinkLibrary/my_str-dll/my_str.h
Normal file
17
DynamicLinkLibrary/my_str-dll/my_str.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifdef MY_STR_BUILD_DLL
|
||||
#define MY_STR_API __declspec(dllexport) //If isBuild, Use EXPORT
|
||||
#else
|
||||
#define MY_STR_API __declspec(dllimport) //If is not used for Build, Use IMPORT
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
//Uses
|
||||
MY_STR_API double evaluate(const char* exp);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
326
DynamicLinkLibrary/readme.md
Normal file
326
DynamicLinkLibrary/readme.md
Normal file
@@ -0,0 +1,326 @@
|
||||
好的,我们来系统讲一讲 **动态链接库(DLL/Shared Object)** 的概念、原理、基本设计原则,以及在 C/C++ 中的写法。为了全面,我会从概念到实战一步步展开。
|
||||
|
||||
---
|
||||
|
||||
## 一、什么是动态链接库(DLL/Shared Object)
|
||||
|
||||
1. **定义**
|
||||
|
||||
* **DLL (Dynamic Link Library)**:Windows 系统下的动态链接库,文件后缀 `.dll`
|
||||
* **SO (Shared Object)**:Linux/Unix 系统下的动态库,文件后缀 `.so`
|
||||
* **本质**:是一段可执行代码(函数、类、资源)打包成的库,可以在程序运行时被加载,而不是编译时静态链接。
|
||||
|
||||
2. **与静态库的区别**
|
||||
|
||||
| 特性 | 静态库 (.lib / .a) | 动态库 (.dll / .so) |
|
||||
| ------- | --------------- | ---------------- |
|
||||
| 链接时间 | 编译时 | 运行时 |
|
||||
| 可执行文件大小 | 大(包含库) | 小(库外部) |
|
||||
| 更新 | 需要重新编译 | 只需替换库文件 |
|
||||
| 共享内存 | 不共享 | 可在多个进程共享 |
|
||||
|
||||
3. **工作原理**
|
||||
|
||||
* **编译阶段**:编译器使用 `.h` 文件声明函数或类接口,不包含实现。
|
||||
* **链接阶段**:在 Windows 上生成 `.lib` 导入库,在 Linux 上生成 `.so` 符号表。
|
||||
* **运行阶段**:
|
||||
|
||||
* **显式加载**:程序运行时通过 `LoadLibrary`(Windows)或 `dlopen`(Linux)加载。
|
||||
* **隐式加载**:程序启动时自动加载 DLL/so。
|
||||
|
||||
---
|
||||
|
||||
## 二、基本设计原则
|
||||
|
||||
写动态库时,需要考虑以下设计原则:
|
||||
|
||||
1. **接口与实现分离**
|
||||
|
||||
* 提供 `.h` 头文件定义函数/类接口
|
||||
* 实现放在 `.c/.cpp` 文件中
|
||||
* 用户只依赖头文件和库文件
|
||||
|
||||
2. **避免全局状态**
|
||||
|
||||
* 动态库可能被多个程序或线程使用
|
||||
* 避免使用全局变量,或使用线程安全机制
|
||||
|
||||
3. **符号导出/隐藏控制**
|
||||
|
||||
* 只导出需要被外部调用的函数或类
|
||||
* Windows:`__declspec(dllexport)` / `__declspec(dllimport)`
|
||||
* Linux:`__attribute__((visibility("default")))`
|
||||
|
||||
4. **版本管理**
|
||||
|
||||
* 避免破坏兼容性
|
||||
* 增加新功能时尽量不要修改已有接口
|
||||
|
||||
5. **异常处理**
|
||||
|
||||
* C++ 动态库不要抛跨 DLL 的异常
|
||||
* C 接口更安全(extern "C")
|
||||
|
||||
---
|
||||
|
||||
## 三、C/C++ 动态库写法
|
||||
|
||||
### 1. Windows DLL 示例(C)
|
||||
|
||||
**my_lib.h**
|
||||
|
||||
```c
|
||||
#ifndef MY_LIB_H
|
||||
#define MY_LIB_H
|
||||
|
||||
#ifdef MY_LIB_BUILD_DLL
|
||||
#define MY_API __declspec(dllexport)
|
||||
#else
|
||||
#define MY_API __declspec(dllimport)
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
MY_API int add(int a, int b);
|
||||
MY_API void hello();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
**my_lib.c**
|
||||
|
||||
```c
|
||||
#include "my_lib.h"
|
||||
#include <stdio.h>
|
||||
|
||||
int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
void hello() {
|
||||
printf("Hello from DLL!\n");
|
||||
}
|
||||
```
|
||||
|
||||
**编译命令(GCC/MinGW)**
|
||||
|
||||
```bash
|
||||
gcc -DMY_LIB_BUILD_DLL -shared my_lib.c -o my_lib.dll -Wl,--out-implib,libmy_lib.a
|
||||
```
|
||||
|
||||
**使用 DLL 的 main.c**
|
||||
|
||||
```c
|
||||
#include "my_lib.h"
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
printf("%d\n", add(2, 3));
|
||||
hello();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Linux SO 示例(C)
|
||||
|
||||
**my_lib.c** 与 Windows 基本相同,只是编译方式不同:
|
||||
|
||||
```bash
|
||||
gcc -fPIC -shared my_lib.c -o libmy_lib.so
|
||||
```
|
||||
|
||||
**使用 SO**
|
||||
|
||||
```c
|
||||
#include "my_lib.h"
|
||||
#include <stdio.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
int main() {
|
||||
void* handle = dlopen("./libmy_lib.so", RTLD_LAZY);
|
||||
if (!handle) {
|
||||
fprintf(stderr, "Cannot open library: %s\n", dlerror());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 加载函数
|
||||
int (*add)(int, int) = dlsym(handle, "add");
|
||||
void (*hello)() = dlsym(handle, "hello");
|
||||
|
||||
printf("%d\n", add(2, 3));
|
||||
hello();
|
||||
|
||||
dlclose(handle);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. C++ 类 DLL
|
||||
|
||||
**my_class.h**
|
||||
|
||||
```cpp
|
||||
#ifndef MY_CLASS_H
|
||||
#define MY_CLASS_H
|
||||
|
||||
#ifdef MY_LIB_BUILD_DLL
|
||||
#define MY_API __declspec(dllexport)
|
||||
#else
|
||||
#define MY_API __declspec(dllimport)
|
||||
#endif
|
||||
|
||||
class MY_API MyClass {
|
||||
public:
|
||||
MyClass();
|
||||
void greet();
|
||||
};
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
**my_class.cpp**
|
||||
|
||||
```cpp
|
||||
#include "my_class.h"
|
||||
#include <iostream>
|
||||
|
||||
MyClass::MyClass() {}
|
||||
void MyClass::greet() {
|
||||
std::cout << "Hello from C++ DLL" << std::endl;
|
||||
}
|
||||
```
|
||||
|
||||
**注意**:C++ DLL 导出类要小心跨模块内存分配和异常。
|
||||
|
||||
---
|
||||
|
||||
## 四、常用技巧与注意事项
|
||||
|
||||
1. **显式 vs 隐式链接**
|
||||
|
||||
* **显式**:运行时加载 `LoadLibrary/dlopen`,灵活,便于插件化
|
||||
* **隐式**:编译时依赖 `.lib` 或 `.so`,启动自动加载
|
||||
|
||||
2. **导出 C 接口,兼容 C++**
|
||||
|
||||
```cpp
|
||||
extern "C" MY_API int add(int a, int b);
|
||||
```
|
||||
|
||||
* 避免 C++ 名字修饰(Name Mangling)导致外部无法调用
|
||||
|
||||
3. **线程安全**
|
||||
|
||||
* 避免 DLL 内部静态或全局对象在多线程环境下出错
|
||||
* 可以使用局部静态或线程局部存储
|
||||
|
||||
4. **资源管理**
|
||||
|
||||
* DLL 占用系统资源,注意释放句柄 `dlclose` / `FreeLibrary`
|
||||
|
||||
---
|
||||
|
||||
✅ 总结:
|
||||
|
||||
* **DLL/SO** 是运行时可加载的库,接口/实现分离,可减少可执行文件体积并支持更新。
|
||||
* **设计原则**:接口分离、导出控制、避免全局状态、版本管理、异常处理。
|
||||
* **写法**:
|
||||
|
||||
* Windows:`__declspec(dllexport/dllimport)` + `.dll` + 可选 `.lib` 导入库
|
||||
* Linux:`-fPIC -shared` + `.so` + `dlopen/dlsym/dlclose`
|
||||
* **C++**:要注意名字修饰、异常、内存分配问题,必要时导出 C 接口。
|
||||
|
||||
---
|
||||
|
||||
如果你愿意,我可以给你画一个 **DLL/so 使用流程图 + C/C++ 示例结构图**,直观展示头文件、实现文件、库文件、调用端之间的关系,非常适合新手理解。
|
||||
|
||||
你希望我画吗?
|
||||
没错!你理解得很对,`.h` 文件在这里就是 **接口文件**(Interface):
|
||||
|
||||
* 它 **声明**了函数、类或者变量,但不包含真正的实现。
|
||||
* 用户只需要包含这个 `.h` 文件就能调用库里的功能,而不需要关心实现细节。
|
||||
* `.c/.cpp` 文件才是真正的 **实现文件**(Implementation),负责具体逻辑。
|
||||
|
||||
可以把它类比成 **“功能说明书”**:告诉你这个库能干什么、怎么调用,但具体怎么干是在背后隐藏的。
|
||||
|
||||
如果你愿意,我可以顺便给你画个小图,把 `.h`、`.c/.cpp`、DLL/so、调用端之间的关系可视化,一眼就懂。你想要吗?
|
||||
|
||||
|
||||
完全正确 ✅
|
||||
|
||||
你理解得没错:**头文件只“告诉编译器函数长啥样”,不负责引入其它库的依赖**。
|
||||
|
||||
详细拆一下:
|
||||
|
||||
---
|
||||
|
||||
## 1️⃣ 你的 `my_str.h` 现在
|
||||
|
||||
```c
|
||||
MY_STR_API double evaluate(const char* exp);
|
||||
```
|
||||
|
||||
* 只声明了 `evaluate()`
|
||||
* 没有 `#include <stdio.h>`
|
||||
* 也没有 `#include <stdlib.h>` 等
|
||||
|
||||
> 也就是说,**头文件本身没有引入 printf、malloc 等依赖**
|
||||
|
||||
---
|
||||
|
||||
## 2️⃣ main.c 如果要用 printf
|
||||
|
||||
```c
|
||||
#include "my_str.h"
|
||||
|
||||
int main() {
|
||||
printf("%f\n", evaluate("1+2*3"));
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
* 编译器此时看到 `printf` 的时候 **不知道它的类型**
|
||||
* 会报错或者警告:`implicit declaration of printf`
|
||||
|
||||
所以你必须:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include "my_str.h"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3️⃣ 原因
|
||||
|
||||
* `my_str.h` **只负责提供 DLL 接口**
|
||||
* 不负责用户程序可能会用的其它东西
|
||||
* 这是 **模块化的设计原则**
|
||||
* 否则你头文件里每一个函数都要 `#include <stdio.h>`,用户不想用 printf 也被迫引入,非常不优雅
|
||||
|
||||
---
|
||||
|
||||
## 4️⃣ 小结经验法则
|
||||
|
||||
1. **头文件只放自己模块的接口和必要类型定义**
|
||||
2. **用户程序用什么库函数,就自己 include**
|
||||
3. **DLL 内部用什么库函数,放在 DLL cpp/c 文件里 include**
|
||||
|
||||
> 换句话说,**头文件不会也不应该“偷带”用户的依赖”**
|
||||
> 这和 C++ 的 class 设计很像:class 内部用什么 STL,外部不用关心
|
||||
|
||||
---
|
||||
|
||||
你如果愿意,我可以画一张 **DLL / 头文件 / 用户 main.c 的依赖关系图**,让你一眼就明白谁负责什么。
|
||||
|
||||
你想让我画吗?
|
||||
Reference in New Issue
Block a user