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

05 Jan 2019 | C/CPP, 异常 | | ˚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.clibgcc最终会根据不同的异常规则(SEH、sjlj)等生成不同的代码,比较复杂。


Older · View Archive (15)

C++奇技淫巧之访问private变量

Newer

理解asio中strand的用法