使用链表而不是 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 longi 表示 intf 表示 `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,而是使用链表实现的可变参数函数就可以工作了。

posted @ 2023-10-15 23:44  Qidi_Huang  阅读(19)  评论(0编辑  收藏  举报