
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) 等生成不同的代码,比较复杂。