【问题标题】:C (void *) used as polymorphic function pointerC(void *) 用作多态函数指针
【发布时间】:2014-04-10 19:30:06
【问题描述】:

我正在尝试创建一个系统调用处理程序,但我不确定如何存储它。

我使用下面的typedef 来存储一个(void *) 指针,它应该接收函数的地址和一个表示参数数量的整数arg_no。然后,我创建了一个这种类型的数组。

typedef struct
{
  void *foo;
  int arg_no;
}td_sys_call_handler;

td_sys_call_handler ish[SYSCALL_HANDLER_NUM];

我正在尝试按以下方式初始化数组。

  ish[0].foo  = void     (*halt) (void);                  ish[0].arg_no  = 0;
  ish[1].foo  = void     (*exit) (int status) NO_RETURN;  ish[1].arg_no  = 1;
  ish[2].foo  = pid_t    (*exec) (const char *file);      ish[2].arg_no  = 1;
  ish[3].foo  = int      (*wait) (pid_t);                 ish[3].arg_no  = 1;
  ish[4].foo  = bool     (*create) (const char *file, unsigned initial_size);
                                                          ish[4].arg_no  = 2;
  ish[5].foo  = bool     (*remove) (const char *file);    ish[5].arg_no  = 1;
  ish[6].foo  = int      (*open) (const char *file);      ish[6].arg_no  = 1;
  ish[7].foo  = int      (*filesize) (int fd);            ish[7].arg_no  = 1;
  ish[8].foo  = int      (*read) (int fd, void *buffer, unsigned length);
                                                          ish[8].arg_no  = 3;
  ish[9].foo  = int      (*write) (int fd, const void *buffer, unsigned length);
                                                          ish[9].arg_no  = 3;
  ish[10].foo = void     (*seek) (int fd, unsigned position);
                                                          ish[10].arg_no = 2;
  ish[11].foo = unsigned (*tell) (int fd);                ish[11].arg_no = 1;

但是从函数指针到void指针的所有赋值都会产生以下错误:

../../userprog/syscall.c: In function ‘syscall_init’:
../../userprog/syscall.c:76:17: error: expected expression before ‘void’
../../userprog/syscall.c:77:17: error: expected expression before ‘void’
../../userprog/syscall.c:78:17: error: expected expression before ‘pid_t’
../../userprog/syscall.c:79:17: error: expected expression before ‘int’
../../userprog/syscall.c:80:17: error: expected expression before ‘_Bool’
../../userprog/syscall.c:82:17: error: expected expression before ‘_Bool’
../../userprog/syscall.c:83:17: error: expected expression before ‘int’
../../userprog/syscall.c:84:17: error: expected expression before ‘int’
../../userprog/syscall.c:85:17: error: expected expression before ‘int’
../../userprog/syscall.c:87:17: error: expected expression before ‘int’
../../userprog/syscall.c:89:17: error: expected expression before ‘void’
../../userprog/syscall.c:91:17: error: expected expression before ‘unsigned’

我的印象是void* 是该语言中唯一的多态实例,它可以指向任何东西。 但是,看来我错了。

那么可以存储任意函数类型地址的指针是哪种类型的呢?

另外,你能给我一个关于 C 多态性的很好的参考吗?我看过很多书,但据我所知,多态性章节非常薄。

谢谢。

【问题讨论】:

  • 使用ish[0].foo = halt,以此类推。函数名的计算结果为一个指针。

标签: c pointers polymorphism void-pointers


【解决方案1】:

函数指针可以转换为void *,但将其转换回正确的函数指针类型以便调用它有点棘手。应该可以使用union。对于要存储的函数类型,您需要一个正确类型的单独联合成员。而且,正如 user4815162342 在评论中指出的那样,您需要管理所有各种组合,可能使用enum

typedef struct
{
  union {
    void *vp;
    void (*v__v)(void);
    void (*v__i)(int);
    pid_t (*pid__ccp)(const char *);
    int (*i__pid)(pid_t);
    bool (*b__ccp_u)(const char *, unsigned);
    bool (*b__ccp)(const char *);
    int (*i__ccp)(const char *);
    int (*i__i)(int);
    int (*i__i_vp_u)(int, void *, unsigned);
    int (*i__i_cvp_u)(int, const void *, unsigned);
    void (*v__i_u)(int, unsigned);
    unsigned (*u__i)(int);
  } fp;
  int arg_no;
}td_sys_call_handler;

这里的想法是尝试将类型编码到标识符中,作为一种“apps-Hungarian”。这样,这些标识符中的任何一个的含义都是直接可见的。

同时生成这些指针和关联的枚举可能更容易。我认为管理这部分的最简单方法是使用我最喜欢的技巧 X-Macros。警告:它只会变得越来越奇怪。

#define function_types(_) \
    _(v__v, void, void) \
    _(v__i, void, int) \
    _(pid_ccp, pid_t, const char *) \
    _(i__pid, int, pid_t) \
    _(b__ccp_u, const char *, unsigned) \
    _(b__ccp, const char *) \
    _(i__ccp, const char *) \
    _(i__i, int) \
    _(i__i_vp_u, int, void *, unsigned) \
    _(i__i_cvp_u, int, const void *, unsigned) \
    _(v__i_u, int, unsigned) \
    _(u__i, unsigned, int) \
    /* end function_types */

这个“主”宏是一个逗号分隔的标记表,逐行传递给_ 下划线宏,该宏是传入的。

现在可以通过编写额外的宏来使用行来构造结构类型,这些作为_ 传入到表宏中以实例化模板:

#define create_function_pointer(id, ret, ...) \
    ret (*id)(__VA_ARGS__);

#define create_function_type_id(id, ret, ...) \
    f__ ## id

typedef struct {
    union {
        void *vp;
        function_types(create_function_pointer)
    } fp;
    int arg_no;
    enum {
        function_types(create_function_type_id)
    } type;
} td_sys_call_handler;

现在可以填充这些结构的数组:

td_sys_call_handler ish[SYSCALL_HANDLER_NUM];
int i=0;

ish[i++]  = (td_sys_call_handler){ halt,     0, f__v__v };
ish[i++]  = (td_sys_call_handler){ exit,     1, f__v__i };
ish[i++]  = (td_sys_call_handler){ exec,     1, f__pid__ccp };
ish[i++]  = (td_sys_call_handler){ wait,     1, f__i__pid };
ish[i++]  = (td_sys_call_handler){ create,   2, f__b__ccp_u };
ish[i++]  = (td_sys_call_handler){ remove,   1, f__b__ccp };
ish[i++]  = (td_sys_call_handler){ open,     1, f__i__ccp };
ish[i++]  = (td_sys_call_handler){ filesize, 1, f__i__i };
ish[i++]  = (td_sys_call_handler){ read,     3, f__i__i_vp_u };
ish[i++]  = (td_sys_call_handler){ write,    3, f__i__i_cvp_u };
ish[i++]  = (td_sys_call_handler){ seek,     2, f__v__i_u };
ish[i++]  = (td_sys_call_handler){ tell,     1, f__u__i };

现在,调用给定这些结构之一的函数将需要(如您所推测的)switch,每个签名都有一个单独的案例。它需要使用 stdarg 和使用适当的联合成员函数指针的调用来破解参数。

void make_sys_call(td_sys_call_handler ish, ...){
    va_list ap;
    int i;
    const char *ccp;
    pid_t pid;
    bool b;
    void *vp;
    unsigned u;
    const void *cvp;
    va_start(ap, ish);
    switch(ish.type) {
    case f__v__f: ish.fp.v__v();
                  break;
    case f__v__i: i = va_arg(int);
                  ish.fp.v__i(i);
                  break;
    case f__pid__ccp: ccp = va_arg(const char *);
                      ish.fp.pid__ccp(ccp);
                      break;
    // etc.
    }
    va_end(ap);
}

不能直接返回不同的类型。您将需要分配一个联合类型变量来保存返回值并返回它,或者更疯狂的东西。外部堆栈数据类型可以保存各种返回类型的联合。根据分析结果,考虑这一点而不是返回联合可能是合适的。

HTH。

【讨论】:

    【解决方案2】:

    我会请求@problemPotato 原谅他窃取了他的结构定义:

    typedef struct 
    {
       void     (*halt) (void);                  
       void     (*exit) (int status);  
       pid_t    (*exec) (const char *file);      
       int      (*wait) (pid_t);                 
       bool     (*create) (const char *file, unsigned initial_size);
       bool     (*remove) (const char *file);    
       int      (*open) (const char *file);      
       int      (*filesize) (int fd);            
       int      (*read) (int fd, void *buffer, unsigned length);
       int      (*write) (int fd, const void *buffer, unsigned length);  
       void     (*seek) (int fd, unsigned position);   
       unsigned (*tell) (int fd);                
    } fs_ops;
    

    假设你有匹配的函数,声明如下:

    int      ext5_open(const char * file);
    unsigned ext5_tell (int fd);
    

    然后您可以定义和初始化一个变量,例如(函数的裸名是指向它的指针):

    fs_ops ext5_ops = {
       .open = ext5_open,
       .tell = ext5_tell,
    };
    

    未初始化的字段为 NULL(即,没有函数的指针)。你可以改变一个字段的值,询问是否设置(if(ext5_ops.seek == NULL) ...),然后调用函数:

    retval = ext5_ops.(*ext5_open)("/tmp/junk");
    

    (*ext5_open) 周围的双亲是因为*(指针间接)绑定不如函数调用强)。

    【讨论】:

    • 谢谢。您是否也知道我如何抽象参数转换?我正在接收堆栈上的所有参数,它们都占用 4 个字节。目前我正在将它们转换为 (void *),但我不知道如何在没有 switch 语句的情况下将它们转换回它们的类型。
    • 打开什么?如果你在堆栈上得到字节,那就是你所拥有的。此外,您的类型今天可能是 4 字节宽,明天 GCC 决定使用 2 或 8 字节,或者下一台机器或编译器,然后一切都会崩溃。了解如何利用编译器的类型处理。如果没有,请重新考虑您在做什么。它闻起来很 OOP(类、虚函数、函数重载),在这种情况下,为什么不使用 C++(或 Objective C,或其他本机支持的语言)?为手头的任务选择正确的工具。更多学习/重新设计/重写;但从长远来看,它的伤害较小。
    【解决方案3】:

    考虑一个格式化为专门保存每个函数的结构:

    typedef struct 
    {
    
      void     (*halt) (void);                  
      void     (*exit) (int status);  
      pid_t    (*exec) (const char *file);      
      int      (*wait) (pid_t);                 
      bool     (*create) (const char *file, unsigned initial_size);
      bool     (*remove) (const char *file);    
      int      (*open) (const char *file);      
      int      (*filesize) (int fd);            
      int      (*read) (int fd, void *buffer, unsigned length);
      int      (*write) (int fd, const void *buffer, unsigned length);  
      void     (*seek) (int fd, unsigned position);   
      unsigned (*tell) (int fd);                
    
    } myFuncs;
    

    这很混乱而且非常难以维护,但是如果您确实使用void *addressOfWait = (void*)&wait; 将每个指针转换为void*,那么您可以在调用之前重新转换为正确的函数指针类型:

    int (*waitFunctionPointer)(pid_t) = addressOfWait;
    

    然后你可以调用那个指针:

    waitFunctionPointer((pid_t) 1111); //wait for process with pid of 1111
    

    【讨论】:

      【解决方案4】:

      您的语法错误。您应该首先声明您的函数指针。然后就可以用函数指针的地址来赋值给指针了。

      void (*halt) (void) = halt_sys_call_function;
      ish[0].foo  = &halt; ish[0].arg_no  = 0;
      

      C 不直接支持传统的继承关系,但它确实保证结构的地址也是结构的第一个成员的地址。这可以用来模拟 C 中的多态性。我在我写的关于 dynamic dispatch in C 的答案中描述了类似的方法。

      【讨论】:

      • 不完全清楚,但我冒昧地猜测他的halt_sys_call_function已经命名为halt,所以第一行是不必要的。跨度>
      • @user4815162342:他在赋值中混合了函数指针声明。如果意图是声明一个函数指针,那么它需要是自己的声明,并且需要为其分配一个值。
      • 我的印象是 OP 很困惑,只是打算将函数的地址存储到 void * (正如其他人指出的那样,这不符合标准)。在这种情况下,中间函数指针变量没有任何好处。
      • @user4815162342:我的回答通过使用 void 指针来存储函数指针变量的地址来解决这个问题。
      • 这实际上是一个好主意,+1。您可能要提到halt 应该是静态存储,或者它的生命周期不应超过ish 的生命周期。
      【解决方案5】:

      您需要的符号将系统调用函数指针转换为void *

      ish[0].foo  = (void *)halt;
      

      C 标准不保证指向函数的指针适合指向数据的指针,例如void *;幸运的是,POSIX 介入并保证指向函数的指针与指向数据的指针大小相同。

      【讨论】:

      • 在 POSIX 2008 中,此要求在某种程度上隐藏在 dlsym(应用程序使用)中。 §2.12.3 不见了,我想。
      • @MichaelFoukarakis:这很奇怪。第 2.13.3 节以前在那儿——我在 SO 上经常引用它,我从Data types 得到文本,从在线参考中复制材料。理由仍然提到它:pointer types(措辞不佳,但是……)。因此,要么 POSIX 2013 更改了规则而没有合理化更改,要么网站上出现了故障。但我同意今天缺少信息。
      • @MichaelFoukarakis:我引用当时 POSIX 标准的众多帖子之一:Can the size of pointers vary depending on what's pointed to?
      • 我已经向 OpenGroup 提交了一个请求,要求澄清 2.13.3 的遗漏是意外丢失还是故意丢失。我不知道我能多快得到回复。
      • 显然有一个问题已提交 (#74) 以删除该文本。
      【解决方案6】:

      是的,你错了。

      void * 指针可以指向任何类型的数据,但在 C 代码中(函数)不是数据。

      即使在 void * 和函数指针之间进行强制转换也是无效的:即使在大多数现代计算机上它都能按预期工作,但语言并不能保证这一点。

      我从您的代码中不明白您打算如何在实践中使用“重载”,您希望如何通过 foo 指针调用?仅拥有预期数量的参数是不够的,参数具有类型,因此在函数调用中的处理方式不同。

      【讨论】:

      • @problemPotato 这仅适用于返回 void 的函数,因此它将失败,例如wait,它返回一个int。 OP 至少需要为他必须处理的不同返回类型使用联合。我开始写一个详细描述这一点的答案,但随后注意到函数的不同参数类型,这使得在没有返回类型和每个参数类型的描述的情况下,无法可移植地调用存储的函数,以及一个巨大的switch 及其所有组合。
      • 使用Harvard design 的处理器就是一个很好的例子。由于代码和数据位于完全不相交的地址空间中,因此不可能在数据指针中保存函数指针。 Atmel 处理器(为 arduino 提供动力等)使用该方案。
      • @spectras 好点,绝对。我想它对于编译器实现者来说仍然是可修复的,但代价是使void * 足够宽以包含一个额外的位来判断它是指向数据还是代码。当然,他们不需要这样做。
      • 这将付出巨大的代价:每一个指针访问都必须检查该位。这意味着测试本身、条件分支、访问数据空间的指令、无条件跳转、访问程序空间的指令(它们是完全不同的指令)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-07-31
      • 2018-04-19
      • 2012-12-17
      • 2012-11-21
      • 1970-01-01
      • 1970-01-01
      • 2013-11-14
      相关资源
      最近更新 更多