6. 对外接口设计

大家好,我是程序喵。


在前面章节我介绍过项目的结构设计,这篇文章我主要向大家介绍如何设计对外接口。


因为我们做的是SDK,SDK的产物一般是库二进制文件 + 头文件,头文件主要包含对外暴露的接口,用户使用SDK的功能其实也就是调用这些对外暴露的头文件中的接口(不要较真,我们这里不考虑骚操作)。


一个合格的SDK,一定要让用户接入方便,调用的接口越少,接入方式越简单越好,减少各个函数排列组合的个数,防止用户调用函数顺序错误导致SDK功能异常。


这里,SDK会对外导出C接口,而不是暴露C++接口,为什么?


C接口的语言交互性是最高的,用户不仅可以用C语言调用,也可以用C++调用,也可以用Go、Python、Rust等其它语言调用。


C接口ABI兼容性最高,用llvm编译器导出的C++符号,可能使用MSVC或者gcc就无法调用,但是C接口就永远不会这样。


我这里先贴出对外暴露的C接口

#pragma once

#include <stdint.h>

#define BURIED_EXPORT __declspec(dllexport)

extern "C" {

typedef struct Buried Buried;

struct BuriedConfig {
const char* host;
const char* port;
const char* topic;
const char* user_id;
const char* app_version;
const char* app_name;
const char* custom_data;
};

BURIED_EXPORT Buried* Buried_Create(const char* work_dir);

BURIED_EXPORT void Buried_Destroy(Buried* buried);

BURIED_EXPORT int32_t Buried_Start(Buried* buried, BuriedConfig* config);

BURIED_EXPORT int32_t Buried_Report(Buried* buried, const char* title,
const char* data, uint32_t priority);
}


这里对外只暴露4个接口,和一个struct。


因为我们需要支持多实例,所以设计出 Buried_Create 和 Buried_Destroy 接口。


Buried_Create 用于创建一个实例,work_dir作为这个实例的工作目录,一个实例一定会在对应的工作目录下生成文件等,不会污染其它目录。


Buried_Destroy 用于销毁一个实例,有创建就有销毁,这里应该不用过多介绍。


Buried_Start 用于开启埋点上报能力,第一个参数buried,传入具体的实例;表示在哪个实例下工作,第二个参数config,表示这个实例对应的配置,其中:


  1. host:要上报的url
  2. port:对应的端口号
  3. topic:对应的topic,一般会上传到url:port/topic下
  4. user_id:用户的ID,方便分析信息,比如哪个用户使用app出了异常等
  5. app_version:应用的版本号
  6. app_name:应用的名称
  7. custom_data:自定义数据,Json的字符串,因为各个用户肯定想携带不同的数据,使用这个custom_data正好。


Buried_Report 用于上报具体的数据,第一个参数buried,传入具体的实例;第二个参数title,这条数据的标题,这个标题可以用于展示在网站端;第三个参数data,表示详细数据;第四个priority,表示优先级,内部模块上报时会按照优先级来上报数据,优先级高的数据会优先上报。


虽然对外暴露的是C接口,但是对内可以使用C++开发。


因为对外Header中只有一个 typedef struct Buried Buried; 语句,我们还不知道这个Buried的具体定义。


它在内部其实有的是C++的定义,定义如下:

#pragma once

#include <stdint.h>

#include <filesystem>
#include <memory>
#include <string>

#include "buried_common.h"
#include "include/buried.h"

namespace spdlog {
class logger;
}

namespace buried {
class BuriedReport;
}

struct Buried {
public:
struct Config {
std::string host;
std::string port;
std::string topic;
std::string user_id;
std::string app_version;
std::string app_name;
std::string custom_data;
};

public:
Buried(const std::string& work_dir);

~Buried();

BuriedResult Start(const Config& config);

BuriedResult Report(std::string title, std::string data, uint32_t priority);
};


然后,我们做一个桥接层就可以巧妙的做这种转换:


#include "include/buried.h"

#include <iostream>

#include "buried_core.h"

extern "C" {

Buried* Buried_Create(const char* work_dir) {
if (!work_dir) {
return nullptr;
}
return new Buried(work_dir);
}

void Buried_Destroy(Buried* buried) {
if (buried) {
delete buried;
}
}

int32_t Buried_Start(Buried* buried, BuriedConfig* config) {
if (!buried || !config) {
return BuriedResult::kBuriedInvalidParam;
}
Buried::Config buried_config;
if (config->host) {
buried_config.host = config->host;
}
// ...
return buried->Start(buried_config);
}

int32_t Buried_Report(Buried* buried, const char* title, const char* data,
uint32_t priority) {
if (!buried || !title || !data) {
return BuriedResult::kBuriedInvalidParam;
}
return buried->Report(title, data, priority);
}
}


代码其实也比较简单,Buried_Create对应new操作,Buried_Destroy对应delete操作。


Buried_Start 和 Buried_Report 中的第一个参数buried,在内部会自动转成C++的指针,进而调用相关的函数。


介绍完对外的接口设计与C和C++的桥接层后,有一点我们还需要了解。


因为我们对外给出的是个DLL动态库,动态库需要导出符号才可以给外部调用,否则外部无法使用这些函数。


如何导出符号?


直接使用 #define BURIED_EXPORT __declspec(dllexport) 就OK。


然后可以使用dumpbin /exports 方法查看是否导出符号成功


Win导出符号有两种方法,详见:https://learn.microsoft.com/zh-cn/cpp/build/determining-which-exporting-method-to-use?view=msvc-170


到这里,本节内容已经介绍完毕,查看本节详细代码可以移步:https://github.com/chengxumiaodaren/BuriedPoint/tree/0.1.4.1


0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
程序喵
下载 APP