代码之家  ›  专栏  ›  技术社区  ›  tomlogic

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

  •  5
  • tomlogic  · 技术社区  · 14 年前

    我正在尝试设计一个可移植的API ANSI C89/ISO C90标准 通过串行接口访问无线网络设备。该库将有多个网络层,需要在其上运行各种版本 嵌入式设备 小到一个8位微型计算机,有32K的代码和2K的数据,在多达1兆字节或更多的代码和数据的嵌入式设备上。

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

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

    我想出了两种可能的解决办法,但都不是特别好。请记住,完整的驱动程序可能是20000行或更多,涵盖多个文件,并包含数百个函数。

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

    // 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;
       }
    

    有没有一个更干净的方法我还没想出来?我倾向于第二个,因为它应该更容易维护,而且更清楚的是,函数的参数中有一些宏魔力。另外,第一个方法需要在函数名前面加上“\”作为函数指针。

    我确实想删除“single interface”情况下的参数,以消除将参数推送到堆栈上的不必要代码,并允许函数访问寄存器中的第一个“real”参数,而不是从堆栈中加载它。如果可能的话,我不想维护两个独立的代码基。

    思想?思想?现有代码中类似的示例?

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

    3 回复  |  直到 14 年前
        1
  •  2
  •   jmucchiello    14 年前

    我喜欢你的第二个解决方案。我只希望每个函数声明两次,而不是在public头中声明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);
    
        2
  •  2
  •   Matt Curtis    14 年前

    您可以使用全局 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);
    }
    

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

        3
  •  1
  •   nategoose    14 年前

    #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)