关于C函数能否抛出异常的讨论

1,213次阅读
没有评论
关于 C 函数能否抛出异常的讨论

C 语言在语言层面上没有“异常”的概念,也不提类似 C ++ 那样能抛出异常的手段。但是,如果在 C ++ 中调用 C 的函数,不能就此完全忽略异常检查。特别是自从 C ++11 开始,不能钦定一个 C 函数调用是noexcept。至于为什么,下面说几个蛋疼的案例。

案例 1

我们知道,通过extern "C",C++ 编译器完全可以导出一个 C 函数接口,尽管函数内部可能包含 C ++ 的调用。如果这些 C ++ 代码会抛异常,那么最终的 C 接口运行时也会抛出异常。代码如下:

libfoo.h

void foo();

libfoo.cpp

#include <stdexcept>

extern "C" {

#include "libfoo.h"

void foo()
{throw std::logic_error("foo logic error");
}
}

testfoo.cpp

#include <iostream>
#include <stdexcept>
extern "C" {#include "libfoo.h"}

int main()
{
    try {foo();
    } catch (std::logic_error e) {std::cout << e.what();
    }
    return 0;
}

首先用 g++ -shared -fPIC -o libfoo.so libfoo.cpp 编译出动态库libfoo.so,然后g++ -o testfoo testfoo.cpp -L./ -lfoo,最后执行./testfoo:

foo logic error

可见 logic_error 被成功捕获,尽管从 testfoo.cpp 角度看,foo();完全是个 C 函数调用。

案例 2

如果说前面的是因为在 C 函数实现过程中掺杂了 C ++ 的成分,那么一个用纯 C 实现的函数就不抛异常了吗?如果参数包含函数指针,就不确定了。看下面代码:

testbar.cpp

#include <iostream>
#include <stdexcept>
extern "C" {#include <stdlib.h>}

int cmp(const void *l, const void *r)
{throw std::logic_error("bar logic error");
    return *(const int *)(l) - *(const int *)(r);
}

int main()
{int arr[5] = {1, 2, 3, 4, 5};
    int k = 3;
    const void *res;
    try {res = bsearch(&k, arr, 5, sizeof(k), cmp);
    } catch (std::logic_error e) {std::cout << e.what() << std::endl;
    }
    return 0;
}

对 C 标准库的 bsearch 的调用传入了一个抛异常的函数指针,最终 bsearch 也会抛异常。编译执行后,输出bar logic error。

案例 3

上面两种情况的异常根本上都是 C ++ 代码抛出的,还有一种情况则不同。直接看代码:

testbaz.cpp


#include <pthread.h>
#include <iostream>

void* baz(void* arg)
{
    std::cout << __func__ << std::endl;
    try {pthread_exit(NULL);
    } catch (...) {
        std::cout << "exception catched." << std::endl;
        throw;
    }
    return NULL;
}

int main()
{
    pthread_t t;
    pthread_create(&t, NULL, baz, NULL);
    pthread_join(t, NULL);
    std::cout << "finished." << std::endl;
    return 0;
}

编译执行以后,输出:

baz
exception catched.
finished.

为什么调用 pthread_exit 会产生异常?按照 posix 标准来讲,pthread_exit的作用是强制线程退出,这个函数是没有返回的,所以需要它来手动清理调用栈,其行为是一个 forced_unwind,类似于一个异常。在libstdc++ 的实现中,这是一个 abi::__forced_unwind 类型 (man 3 abi::__forced_unwind 有真相)。注意这个异常 catch 到后必须重新抛出,否则无法正常完成栈清理。

同时要注意不能在析构函数中调用pthread_exit,因为析构函数不允许抛异常。

其实 pthread_cancel 也会有相同的行为。以下来自 glibc-2.28 源码:

nptl/pthread_cancel.c

int
__pthread_cancel (pthread_t th)
{
      ......

      pid_t pid = __getpid ();

	  INTERNAL_SYSCALL_DECL (err);
	  int val = INTERNAL_SYSCALL_CALL (tgkill, err, pid, pd->tid,
					   SIGCANCEL);
      ......
}

发了一个 SIGCANCEL 信号。信号处理注册在:

nptl/nptl-init.c


static void
sigcancel_handler (int sig, siginfo_t *si, void *ctx)
{
  ......
	  /* Set the return value.  */
	  THREAD_SETMEM (self, result, PTHREAD_CANCELED);

	  /* Make sure asynchronous cancellation is still enabled.  */
	  if ((newval & CANCELTYPE_BITMASK) != 0)
	    /* Run the registered destructors and terminate the thread.  */
	    __do_cancel ();

	  break;
	}
  ......
}

void
__pthread_initialize_minimal_internal (void)
{
  ......
  struct sigaction sa;
  __sigemptyset (&sa.sa_mask);

# ifdef SIGCANCEL
  /* Install the cancellation signal handler.  If for some reason we
     cannot install the handler we do not abort.  Maybe we should, but
     it is only asynchronous cancellation which is affected.  */
  sa.sa_sigaction = sigcancel_handler;
  sa.sa_flags = SA_SIGINFO;
  (void) __libc_sigaction (SIGCANCEL, &sa, NULL);
  ......
}

再看pthread_exit:

nptl/pthread_exit.c

void
__pthread_exit (void *value)
{THREAD_SETMEM (THREAD_SELF, result, value);

  __do_cancel ();}

最终都是调用的__do_cancel():

nptl/pthreadP.h

static inline void
__attribute ((noreturn, always_inline))
__do_cancel (void)
{
  struct pthread *self = THREAD_SELF;

  /* Make sure we get no more cancellations.  */
  THREAD_ATOMIC_BIT_SET (self, cancelhandling, EXITING_BIT);

  __pthread_unwind ((__pthread_unwind_buf_t *)
		    THREAD_GETMEM (self, cleanup_jmp_buf));
}

nptl/unwind.c

void
__cleanup_fct_attribute __attribute ((noreturn))
__pthread_unwind (__pthread_unwind_buf_t *buf)
{struct pthread_unwind_buf *ibuf = (struct pthread_unwind_buf *) buf;
  struct pthread *self = THREAD_SELF;

  /* This is not a catchable exception, so don't provide any details about
     the exception type.  We do need to initialize the field though.  */
  THREAD_SETMEM (self, exc.exception_class, 0);
  THREAD_SETMEM (self, exc.exception_cleanup, &unwind_cleanup);

  _Unwind_ForcedUnwind (&self->exc, unwind_stop, ibuf);
  /* NOTREACHED */

  /* We better do not get here.  */
  abort ();}

旧版本中,__pthread_unwind是通过 setjmp/longjmp 实现的;在这一版 glibc 中,_Unwind_ForcedUnwind最终会调用 libgcc 中的 _Unwind_ForcedUnwind,见sysdeps/nptl/unwind-forcedunwind.c,libgcc 最终会根据不同的异常规则 (SEH、sjlj) 等生成不同的代码,比较复杂。

正文完
 0
评论(没有评论)
验证码