设计一个带有编译时选项的 API,以删除大多数函数的第一个参数并使用全局变量

发布于 2024-09-01 00:44:11 字数 3158 浏览 2 评论 0原文

我正在尝试在 ANSI C89/ISO C90 中设计一个可移植的 API,以访问串行接口上​​的无线网络设备。该库将具有多个网络层,并且各种版本需要在嵌入式设备上运行,小至具有32K代码和2K数据的8位微控制器,大至具有1兆字节或1兆字节的嵌入式设备。更多的代码和数据。

在大多数情况下,目标处理器将具有单个网络接口,并且我希望使用包含该设备的所有状态信息的单个全局结构。我不想通过网络层传递指向该结构的指针。

在少数情况下(例如,具有更多资源的设备需要驻留在两个网络上),我将连接到多个设备,每个设备都有自己的全局状态,并且需要传递指向该状态的指针(或状态的索引)数组)通过层。

我想出了两种可能的解决方案,但没有一种是特别漂亮的。请记住,完整的驱动程序可能有 20,000 行或更多,涵盖多个文件,并包含数百个函数。

第一个解决方案需要一个宏,该宏会丢弃每个需要访问全局状态的函数的第一个参数:

// network.h
   typedef struct dev_t {
      int var;
      long othervar;
      char name[20];
   } dev_t;

   #ifdef IF_MULTI
      #define foo_function( x, a, b, c)      _foo_function( x, a, b, c)
      #define bar_function( x)               _bar_function( x)
   #else
      extern dev_t DEV;
      #define IFACE (&DEV)
      #define foo_function( x, a, b, c)      _foo_function( a, b, c)
      #define bar_function( x)               _bar_function( )
   #endif

   int bar_function( dev_t *IFACE);
   int foo_function( dev_t *IFACE, int a, long b, char *c);

// network.c
       #ifndef IF_MULTI
          dev_t DEV;
       #endif
   int bar_function( dev_t *IFACE)
   {
      memset( IFACE, 0, sizeof *IFACE);

      return 0;
   }

   int foo_function( dev_t *IFACE, int a, long b, char *c)
   {
      bar_function( IFACE);
      IFACE->var = a;
      IFACE->othervar = b;
      strcpy( IFACE->name, c);

      return 0;
   }

第二个解决方案定义要在函数声明中使用的宏:

// network.h
   typedef struct dev_t {
      int var;
      long othervar;
      char name[20];
   } dev_t;

   #ifdef IF_MULTI
      #define DEV_PARAM_ONLY        dev_t *IFACE
      #define DEV_PARAM             DEV_PARAM_ONLY,
   #else
      extern dev_t DEV;
      #define IFACE (&DEV)
      #define DEV_PARAM_ONLY        void
      #define DEV_PARAM
   #endif

   int bar_function( DEV_PARAM_ONLY);
   // I don't like the missing comma between DEV_PARAM and arg2...
   int foo_function( DEV_PARAM int a, long b, char *c);

// network.c
       #ifndef IF_MULTI
          dev_t DEV;
       #endif
   int bar_function( DEV_PARAM_ONLY)
   {
      memset( IFACE, 0, sizeof *IFACE);

      return 0;
   }

   int foo_function( DEV_PARAM int a, long b, char *c)
   {
      bar_function( IFACE);
      IFACE->var = a;
      IFACE->othervar = b;
      strcpy( IFACE->name, c);

      return 0;
   }

访问任一方法的 C 代码保持不变:

// multi.c - example of multiple interfaces
   #define IF_MULTI
   #include "network.h"
   dev_t if0, if1;

   int main()
   {
      foo_function( &if0, -1, 3.1415926, "public");
      foo_function( &if1, 42, 3.1415926, "private");

      return 0;
   }

// single.c - example of a single interface
   #include "network.h"
   int main()
   {
      foo_function( 11, 1.0, "network");

      return 0;
   }

是否有更简洁的方法我还没弄清楚吗?我倾向于第二种,因为它应该更容易维护,而且更清楚的是,函数的参数中有一些宏观魔法。另外,当我想将它们用作函数指针时,第一种方法需要在函数名称前加上“_”前缀。

我确实想删除“单一接口”情况下的参数,以消除将参数推入堆栈的不必要的代码,并允许函数访问寄存器中的第一个“真实”参数,而不是从堆栈加载它。而且,如果可能的话,我不想维护两个单独的代码库。

想法?有想法吗?现有代码中有类似的例子吗?

(请注意,不能选择使用 C++,因为某些计划的目标没有可用的 C++ 编译器。)

I'm trying to design a portable API in ANSI C89/ISO C90 to access a wireless networking device on a serial interface. The library will have multiple network layers, and various versions need to run on embedded devices as small as an 8-bit micro with 32K of code and 2K of data, on up to embedded devices with a megabyte or more of code and data.

In most cases, the target processor will have a single network interface and I'll want to use a single global structure with all state information for that device. I don't want to pass a pointer to that structure through the network layers.

In a few cases (e.g., device with more resources that needs to live on two networks) I will interface to multiple devices, each with their own global state, and will need to pass a pointer to that state (or an index to a state array) through the layers.

I came up with two possible solutions, but neither one is particularly pretty. Keep in mind that the full driver will potentially be 20,000 lines or more, cover multiple files, and contain hundreds of functions.

The first solution requires a macro that discards the first parameter for every function that needs to access the global state:

// network.h
   typedef struct dev_t {
      int var;
      long othervar;
      char name[20];
   } dev_t;

   #ifdef IF_MULTI
      #define foo_function( x, a, b, c)      _foo_function( x, a, b, c)
      #define bar_function( x)               _bar_function( x)
   #else
      extern dev_t DEV;
      #define IFACE (&DEV)
      #define foo_function( x, a, b, c)      _foo_function( a, b, c)
      #define bar_function( x)               _bar_function( )
   #endif

   int bar_function( dev_t *IFACE);
   int foo_function( dev_t *IFACE, int a, long b, char *c);

// network.c
       #ifndef IF_MULTI
          dev_t DEV;
       #endif
   int bar_function( dev_t *IFACE)
   {
      memset( IFACE, 0, sizeof *IFACE);

      return 0;
   }

   int foo_function( dev_t *IFACE, int a, long b, char *c)
   {
      bar_function( IFACE);
      IFACE->var = a;
      IFACE->othervar = b;
      strcpy( IFACE->name, c);

      return 0;
   }

The second solution defines macros to use in the function declarations:

// network.h
   typedef struct dev_t {
      int var;
      long othervar;
      char name[20];
   } dev_t;

   #ifdef IF_MULTI
      #define DEV_PARAM_ONLY        dev_t *IFACE
      #define DEV_PARAM             DEV_PARAM_ONLY,
   #else
      extern dev_t DEV;
      #define IFACE (&DEV)
      #define DEV_PARAM_ONLY        void
      #define DEV_PARAM
   #endif

   int bar_function( DEV_PARAM_ONLY);
   // I don't like the missing comma between DEV_PARAM and arg2...
   int foo_function( DEV_PARAM int a, long b, char *c);

// network.c
       #ifndef IF_MULTI
          dev_t DEV;
       #endif
   int bar_function( DEV_PARAM_ONLY)
   {
      memset( IFACE, 0, sizeof *IFACE);

      return 0;
   }

   int foo_function( DEV_PARAM int a, long b, char *c)
   {
      bar_function( IFACE);
      IFACE->var = a;
      IFACE->othervar = b;
      strcpy( IFACE->name, c);

      return 0;
   }

The C code to access either method remains the same:

// multi.c - example of multiple interfaces
   #define IF_MULTI
   #include "network.h"
   dev_t if0, if1;

   int main()
   {
      foo_function( &if0, -1, 3.1415926, "public");
      foo_function( &if1, 42, 3.1415926, "private");

      return 0;
   }

// single.c - example of a single interface
   #include "network.h"
   int main()
   {
      foo_function( 11, 1.0, "network");

      return 0;
   }

Is there a cleaner method that I haven't figured out? I lean toward the second since it should be easier to maintain, and it's clearer that there's some macro magic in the parameters to the function. Also, the first method requires prefixing the function names with "_" when I want to use them as function pointers.

I really do want to remove the parameter in the "single interface" case to eliminate unnecessary code to push the parameter onto the stack, and to allow the function to access the first "real" parameter in a register instead of loading it from the stack. And, if at all possible, I don't want to have to maintain two separate codebases.

Thoughts? Ideas? Examples of something similar in existing code?

(Note that using C++ isn't an option, since some of the planned targets don't have a C++ compiler available.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

夜清冷一曲。 2024-09-08 00:44:11

我喜欢你的第二个解决方案。我只是更喜欢将每个函数声明两次,而不是将 PARAM 宏放在公共标头中。我更喜欢将宏 hijinks 放在隐藏的 C 文件中。

// common header
#ifdef IF_MULTI
    int foo_func1(dev_t* if, int a);
    int foo_func2(dev_t* if, int a, int b);
    int foo_func3(dev_t* if);
#else
    int foo_func1(int a);
    int foo_func2(int a, int b);
    int foo_func3();
#endif

// your C file
#ifdef IF_MULTI
    #define IF_PARM dev_t* if,
    #define GET_IF() (if)
#else
    dev_t global_if;
    #define IF_PARM
    #define GET_IF() (&global_if)
#endif

int foo_func1(IF_PARM int a)
{
    GET_IF()->x = a;
    return GET_IF()->status;
}
int foo_func2(IF_PARM int a, int b)
int foo_func3(IF_PARM);

I like your second solution. I just prefer declaring every function twice rather than have that PARAM macro in the public header. I much prefer to put macro hijinks in the hidden C file.

// common header
#ifdef IF_MULTI
    int foo_func1(dev_t* if, int a);
    int foo_func2(dev_t* if, int a, int b);
    int foo_func3(dev_t* if);
#else
    int foo_func1(int a);
    int foo_func2(int a, int b);
    int foo_func3();
#endif

// your C file
#ifdef IF_MULTI
    #define IF_PARM dev_t* if,
    #define GET_IF() (if)
#else
    dev_t global_if;
    #define IF_PARM
    #define GET_IF() (&global_if)
#endif

int foo_func1(IF_PARM int a)
{
    GET_IF()->x = a;
    return GET_IF()->status;
}
int foo_func2(IF_PARM int a, int b)
int foo_func3(IF_PARM);
傲性难收 2024-09-08 00:44:11

如果您有线程(或在重新进入时切换接口或类似的东西),这里的解决方案将不起作用,但它是一个干净的接口,并且可能适合您。

您可以让单实例函数使用全局 DEV,并让多接口函数设置此全局函数并调用其对应的单实例函数。

例如:


dev_t *DEV;

int foo_function(int x, int y)
{
    /* DEV->whatever; */
    return DEV->status;
}

int foo_function_multi(dev_t *IFACE, int x, int y)
{
    DEV = IFACE;
    return foo_function(x, y);
}

另一个选择是使用可变参数,并传递和获取额外的参数(其中包含要使用的接口)#ifdef MULTI,但这很糟糕,因为你会失去类型安全性,并且会阻止在寄存器中传递参数您可能非常关心您的平台。此外,所有具有可变参数的函数都必须至少有一个命名参数,而您的问题都是关于避免参数!但无论如何:


#ifndef MULTI
dev_t *DEV;
#endif
int foo(int x, int y, ...)
{
#ifdef MULTI
    va_list args;
    va_start(args, y);
    dev_t *DEV = va_arg(args, (dev_t*));
    va_end(args);
#endif
    /* DEV->whatever */
    return DEV->status;
}

// call from single
int quux()
{
    int status = foo(23, 17);
}

// call from multi
int quux()
{
    int status = foo(23, 17, &if0);
}

我个人更喜欢你的第一个解决方案:-)

Here's a solution that won't work if you have threads (or switch interfaces on re-entrance or something like that), but it is a clean interface, and it might work for you.

You could have your single instance functions using a global DEV, and have your multi interface functions set this global and call their single instance counterparts.

For example:


dev_t *DEV;

int foo_function(int x, int y)
{
    /* DEV->whatever; */
    return DEV->status;
}

int foo_function_multi(dev_t *IFACE, int x, int y)
{
    DEV = IFACE;
    return foo_function(x, y);
}

Another option is to use variadic args, and pass and fetch an extra arg (which contains the interface to use) #ifdef MULTI, but that's horrible because you lose your type safety, and would prevent passing the arg in a register which you possibly care quite a bit about on your platform. Also, all functions with variadic args must have at least one named argument, and your question is all about avoiding arguments! But anyway:


#ifndef MULTI
dev_t *DEV;
#endif
int foo(int x, int y, ...)
{
#ifdef MULTI
    va_list args;
    va_start(args, y);
    dev_t *DEV = va_arg(args, (dev_t*));
    va_end(args);
#endif
    /* DEV->whatever */
    return DEV->status;
}

// call from single
int quux()
{
    int status = foo(23, 17);
}

// call from multi
int quux()
{
    int status = foo(23, 17, &if0);
}

Personally I prefer your first solution :-)

冷弦 2024-09-08 00:44:11

这适用于 gcc:

#ifdef TOMSAPI_SMALL
#define TOMSAPI_ARGS( dev, ...) (__VA_ARGS__)
#else  // ! TOMSAPI_SMALL
#define TOMSAPI_ARGS( dev, ...) (dev, ## __VA_ARGS__)
#endif // TOMSAPI_SMALL

#ifdef TOMSAPI_SMALL
#define TOMSAPI_DECLARE_DEVP(local_dev_ptr) device_t * local_dev_ptr = &global_dev; NULL
// The trailing NULL is to make the compiler make you put a ; after calling the macro,
// but without allowing something that would mess up the declaration if you forget the ;
// You can't use the do{...}while(0) trick for a variable declaration.
#else  // ! TOMSAPI_SMALL
#define TOMSAPI_DECLARE_DEVP(local_dev_ptr) device_t * local_dev_ptr = arg_dev; NULL
#endif // TOMSAPI_SMALL

然后

int tomsapi_init TOMSAPI(device_t *arg_dev, void * arg_for_illustration_purposes ) {
    TOMSAPI_DECLARE_DEVP( my_dev );
    my_dev->stuff = arg_for_illustration_purposes;
    return 0;
}

使用此方法,您必须确保所有 API 函数对设备指针使用相同的名称,但所有函数定义和声明看起来都需要完整数量的参数。如果这对你来说不重要,你可以这样做:

#ifdef TOMSAPI_SMALL
#define TOMSAPI_ARGS(...) (__VA_ARGS__)
#else  // ! TOMSAPI_SMALL
#define TOMSAPI_ARGS(...) (device_t *dev, ## __VA_ARGS__)
#endif // TOMSAPI_SMALL

#ifdef TOMSAPI_SMALL
#define TOMSAPI_DECLARE_DEVP() device_t * dev = &global_dev; NULL 
#else  // ! TOMSAPI_SMALL
#define TOMSAPI_DECLARE_DEVP(local_dev_ptr) NULL
#endif // TOMSAPI_SMALL

然后

int tomsapi_init TOMSAPI(void * arg_for_illustration_purposes ) {
    dev->stuff = arg_for_illustration_purposes;
    return 0;
}

但这最终看起来像 dev 从未向阅读你的代码的人声明过。

综上所述,您可能会发现,在单个设备小型平台上,使用全局设备结构的成本最终比传递指针的成本更高,因为必须重新加载该结构的地址的次数。如果您的 API 是堆叠的(您的某些函数调用其他函数并向它们传递 dev 指针)、使用大量尾递归和/或您的平台使用寄存器而不是堆栈来传递大多数参数,则更有可能出现这种情况。

编辑:
我刚刚意识到,如果你的 api 函数不带额外的参数,即使你确实使用了 ## 运算符(如果你的编译器想要强制你说 int foo(void)< /code> 用于不带参数的函数。

This will work on gcc:

#ifdef TOMSAPI_SMALL
#define TOMSAPI_ARGS( dev, ...) (__VA_ARGS__)
#else  // ! TOMSAPI_SMALL
#define TOMSAPI_ARGS( dev, ...) (dev, ## __VA_ARGS__)
#endif // TOMSAPI_SMALL

#ifdef TOMSAPI_SMALL
#define TOMSAPI_DECLARE_DEVP(local_dev_ptr) device_t * local_dev_ptr = &global_dev; NULL
// The trailing NULL is to make the compiler make you put a ; after calling the macro,
// but without allowing something that would mess up the declaration if you forget the ;
// You can't use the do{...}while(0) trick for a variable declaration.
#else  // ! TOMSAPI_SMALL
#define TOMSAPI_DECLARE_DEVP(local_dev_ptr) device_t * local_dev_ptr = arg_dev; NULL
#endif // TOMSAPI_SMALL

and then

int tomsapi_init TOMSAPI(device_t *arg_dev, void * arg_for_illustration_purposes ) {
    TOMSAPI_DECLARE_DEVP( my_dev );
    my_dev->stuff = arg_for_illustration_purposes;
    return 0;
}

Using this method you would have to ensure that all of your API functions used the same name for the device pointer, but all of your function definitions and declarations would look like they needed the full number of arguments. If this were not important to you you could do:

#ifdef TOMSAPI_SMALL
#define TOMSAPI_ARGS(...) (__VA_ARGS__)
#else  // ! TOMSAPI_SMALL
#define TOMSAPI_ARGS(...) (device_t *dev, ## __VA_ARGS__)
#endif // TOMSAPI_SMALL

#ifdef TOMSAPI_SMALL
#define TOMSAPI_DECLARE_DEVP() device_t * dev = &global_dev; NULL 
#else  // ! TOMSAPI_SMALL
#define TOMSAPI_DECLARE_DEVP(local_dev_ptr) NULL
#endif // TOMSAPI_SMALL

and then

int tomsapi_init TOMSAPI(void * arg_for_illustration_purposes ) {
    dev->stuff = arg_for_illustration_purposes;
    return 0;
}

But this ends up looking like dev is never declared to someone reading your code.

All of that being said, you may find that on the single device small platform that using a global device struct ends up costing more than passing the pointer around due to the number of times the address of this struct will have to be reloaded. This is more likely if you API is stacked (some of your functions call other of your functions and pass them the dev pointer), uses a lot of tail recursion, and/or your platform uses registers for passing most arguments rather than the stack.

EDIT:
I just realized that there could be a problem with this method if you have api functions which take no additional arguments, even if you do use the ## operator if your compiler wants to force you to say int foo(void) for functions that take no arguments.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文