使用链表而不是 stdarg 实现可变参数函数
Qidi 2023.10.15
0. 需要使用可变参数函数的场景
常见的场景是类似于 printf(char *fmt, ...)
函数,输入的参数个数和类型都是未知的,此时除了需要 ...
表示可变参数列表,还需要用 fmt
参数说明参数的个数和类型。
还有另一种场景,假设我们要实现一个音频控制功能的程序。在初始设计中,需要实现以下 3 个函数:
int init(char *amplifierType);
int connect(long long audioSource, int speakerMask, float sourceGain);
int setVolume(long long audioSource, int volumeStep);
为了 便于编写测试工具 或 便于用有限状态机管理程序状态,我们需要将函数/接口形式统一,这时也要用到可变参数函数。
本文主要讨论第二种场景。
1. stdarg 的局限
va_start()
必须知道可变参数列表的前一个参数,用作查询可变参数列表的起始地址;va_arg()
等宏的实现位于编译器内部,不便于阅读研究、了解代码细节。
2. stdarg 的工作原理
程序运行时,函数参数或存于寄存器、或存于栈。因为寄存器是已知的,且栈上的参数位于连续内存地址上,所以 stdarg 可以通过 读取寄存器 和通过 栈上起始地址 获得所有参数。
关于 stdarg 原理的更多介绍,推荐阅读 《AArch64中va_list/va_start/va_arg/...的实现》。
3. 使用链表实现可变参数列表
为了摆脱 va_start()
对参数列表起始地址的依赖,我们可以把函数参数按照从左往右的顺序,依次存储于一个动态创建的链表中。
3.1 链表设计
链表节点要设计得足够通用,可以容纳常用数据类型。比如:
typedef union {
long long m_ll;
char *m_s;
int m_i;
float m_f;
} FuncArgItem;
struct FuncArgChainItem {
FuncArgItem mArgVal;
struct FuncArgChainItem *mpNext;
};
typedef struct FuncArgChainItem FuncArgChainItem_t;
3.2 可变参数函数声明
采用 stdarg 实现可变参数函数时,函数声明的形式类似下方这样:
int init(char *fmt, ...);
int connect(char *fmt, ...);
int setVolume(char *fmt, ...);
或者
int init(char *fmt, va_list ap);
int connect(char *fmt, va_list ap);
int setVolume(char *fmt, va_list ap);
在本文开头,我们其实就已经清楚了 init()
、connect()
、setVolume()
函数各自能接收的参数个数和类型,所以使用 stdarg 方式传递可变参数时,就算没有 fmt
参数,我们也能在各个函数的函数体里依次取出参数。这里之所以要写出 fmt
参数,完全是因为 stdarg 的相关语法导致。在我们讨论的场景中,fmt
完全是个冗余参数。
采用链表来传递可变参数,就可以规避冗余参数的问题。基于 3.1 节的链表设计,可变参数函数声明应该改为:
int init(FuncArgChainItem_t *argChainHead); // "char *amplifierName"
int connect(FuncArgChainItem_t *argChainHead); // "long long audioSource int speakerMask, float sourceGain"
int setVolume(FuncArgChainItem_t *argChainHead); // "long long audioSource, int volumeStep"
其中 argChainHead
参数是链表头。链表中存储任意多个任意类型的参数。不同函数的参数个数和参数类型可以不同。
此时我们发现,采用可变参数链表替换掉原先的参数后,函数形式变得统一。每个函数都可以用以下函数指针进行表示:
typedef int(*AudioFuncPtr)(FuncArgChainItem_t *);
3.3 函数读取可变参数的方式
函数读取传入的可变参数的方式非常简单。
因为我们早已清楚每个函数需要接收的参数个数和类型,所以在函数体中可以直接从链表里读取各个参数。比如:
int init(FuncArgChainItem_t *argChainHead) // "char *amplifierName"
{
char *amplifierType = argChainHead->mArgVal.m_s;
printf("%s got args: amplifierType(%s)\n", __func__, amplifierType);
return RET_OK;
}
int connect(FuncArgChainItem_t *argChainHead) // "long long audioSource int speakerMask, float sourceGain"
{
long long audioSource = argChainHead->mArgVal.m_ll;
int channelMask = argChainHead->mpNext->mArgVal.m_i;
float sourceGain = argChainHead->mpNext->mpNext->mArgVal.m_f;
printf("%s got args: audioSource(%lld), channelMask(%d), sourceGain(%f)\n", __func__, audioSource, channelMask, sourceGain);
return RET_OK;
}
int setVolume(FuncArgChainItem_t *argChainHead) // "long long audioSource, int volumeStep"
{
long long audioSource = argChainHead->mArgVal.m_ll;
int volumeStep = argChainHead->mpNext->mArgVal.m_i;
printf("%s got args: audioSource(%lld), volumeStep(%d)\n", __func__, audioSource, volumeStep);
return RET_OK;
}
3.4 如何向函数写入可变参数
写入可变参数的方式稍微复杂一点点。
向各个函数写入的可变参数通过链表存储。各个函数所接收的参数个数和类型不同,因此我们需要动态构造拥有不同长度和节点类型的链表。
为了便于动态构造链表,以及让代码更加通用,我们将每个函数和它所需的真实参数信息记录在一张表里。如下(称之为表,实际是个数组):
typedef enum {
CASE_INIT,
CASE_CONNECT,
CASE_SETVOLUME
} AudioFuncCase;
typedef struct {
int mFuncCase; // AudioFuncCase enumerations
char *mFuncStr;
AudioFuncPtr mFuncPtr;
char *mFuncArg;
char *mFuncArgDesc;
} AudioFuncTableItem;
const AudioFuncTableItem gAudioFuncTable[] = {
// CAUTION: argument descriptions must be extra '\0' terminated, else program may crash!!!
{CASE_INIT, "init", init, "s", "amplifierType\0"},
{CASE_CONNECT, "connect", connect, "ll,i,f", "audioSource\0channelMask\0sourceGain\0"},
{CASE_SETVOLUME, "setVolume", setVolume, "ll,i", "audioSource\0volumeStep\0"}
};
const int gAudioFuncTableSize = sizeof(gAudioFuncTable)/sizeof(AudioFuncTableItem);
其中 mFuncArg
表示各函数真实需要的参数个数和类型。s
表示 char *
,ll
表示 long long
,i
表示 int
,f
表示 `float``。
然后再编写一段代码,根据表里信息为每个函数自动生成可变参数链表,并传入参数。
可变参数列表中,每个参数的值可以从其它地方读取/赋值,也可以是来自手工输入。以手工输入参数值为例,这段代码类似下方:
#define NEW_ARG_ITEM(argVal, argShortType, argChainHead, argChainCurrent) \
\
FuncArgChainItem_t *argChainItem_##argShortType = (FuncArgChainItem_t *)malloc(sizeof(FuncArgChainItem_t)); \
memset(argChainItem_##argShortType, 0, sizeof(FuncArgChainItem_t)); \
argChainItem_##argShortType->mArgVal.m_##argShortType = argVal; \
\
if (argChainHead == NULL) { \
argChainHead = argChainItem_##argShortType; \
} else { \
argChainCurrent->mpNext = argChainItem_##argShortType; \
} \
argChainCurrent = argChainItem_##argShortType;
int callAudioFunc()
{
int ret = RET_ERROR;
FuncArgChainItem_t *argChainHead = NULL, *argChainCurrent = NULL;
char *origArgList = gAudioFuncTable[testCaseChoice].mFuncPtrArg;
int origArgListSize = strlen(origArgList) + 1;
char *copiedArgList = (char *)malloc(origArgListSize);
memcpy(copiedArgList, origArgList, origArgListSize);
char *nextArgDesc = gAudioFuncTable[testCaseChoice].mFuncArgDesc;
char *subArgType, *subArgType_saved;
subArgType = strtok_r(copiedArgList, ",", &subArgType_saved);
while (subArgType != NULL) {
if (strcmp(subArgType, "ll") == 0) {
long long input_ll;
scanf("%lld", &input_ll);
NEW_ARG_ITEM(input_ll, ll, argChainHead, argChainCurrent);
} else if (strcmp(subArgType, "i") == 0) {
int input_i;
scanf("%d", &input_i);
NEW_ARG_ITEM(input_i, i, argChainHead, argChainCurrent);
} else if (strcmp(subArgType, "f") == 0) {
float input_f;
scanf("%f", &input_f);
NEW_ARG_ITEM(input_f, f, argChainHead, argChainCurrent);
} else if (strcmp(subArgType, "s") == 0) {
char input_s[MAX_KVPAIR_SIZE] = {0};
LIMITED_STR_SCANF(MAX_KVPAIR_SIZE-1, input_s);
NEW_ARG_ITEM(input_s, s, argChainHead, argChainCurrent);
} else {
printf("illegal argument type \'%s\'\n", subArgType);
return RET_ERROR;
}
subArgType = strtok_r(NULL, ",", &subArgType_saved);
}
free(copiedArgList);
ret = gAudioFuncTable[testCaseChoice].mFuncPtr(argChainHead);
return ret;
}
至此,我们不使用 stdarg,而是使用链表实现的可变参数函数就可以工作了。