不,这是不可移植的,我不确定你想要达到什么目的。
术语
也许您想要一些非本地跳转。仔细阅读setjmp.h、coroutines、调用堆栈、异常处理、continuation和continuation-passing-style。了解Scheme中的call/cc应该是非常有益的。
setjmp
和longjmp
setjmp和longjmp是标准的 C99 函数(它们非常快,因为保存的状态实际上非常小)。使用它们时要非常小心(特别是要避免任何内存泄漏)。longjmp
(或 POSIX 中相关的siglongjmp)是可移植标准 C99中逃离某些功能并返回到某些调用者的唯一方法。
这个想法是在没有任何额外开销的情况下从深层调用链中提供“转义代码路径”
这正是longjmp
with的作用setjmp
。两者都是快速、恒定时间的操作(特别是展开包含数千个调用帧的调用堆栈longjmp
需要很短且恒定的时间)。内存开销实际上是每个捕获点一个本地jmp_buf
的,没什么大不了的。jmp_buf
很少将其放在调用堆栈之外。
有效使用它们的一种常见方法是将setjmp
-edjmp_buf
放在本地struct
(所以在你的调用框架中)并将指向它的指针传递struct
给一些内部static
函数,这些函数会间接调用longjmp
错误。因此setjmp
,并且longjmp
可以通过明智的编码约定很好和有效地模仿 C++ 异常抛出和处理的复杂语义(或 Ocaml 异常或 Java 异常,它们都具有与 C++ 不同的语义)。它们是便携式基本积木,足以满足此目的。
实际上,代码如下:
struct my_foo_state_st {
jmp_buf jb;
char* rs;
// some other state, e.g a ̀ FILE*` or whatever
};
/// returns a `malloc̀ -ed error message on error, and NULL on success
extern const char* my_foo (struct some_arg_st* arg);
是私人struct my_foo_state_st
国家。这是公共函数(您将在一些公共标头中声明)。您确实记录了(至少在评论中)它在失败时返回堆分配的错误消息,因此调用者负责释放它。成功后,您记录了它返回. 当然,您可以有其他约定和其他参数和/或结果。my_foo
NULL
我们现在声明并实现一个错误函数,它将错误消息打印到状态中并以longjmp
static void internal_error_printf (struct my_foo_state*sta,
int errcode,
const char *fmt, ...)
__attribute__((noreturn, format(printf(2,3))));
void internal_error_printf(struct my_foo_state*sta,
int errcode, const char *fmt, ...) {
va_arg args;
va_start(args, fmt);
vasprintf(&sta->rs, fmt, args);
va_end(args);
longjmp(sta->jb, errcode);
}
我们现在有几个可能复杂的递归函数来完成大部分工作。我只画他们,你知道你想让他们做什么。当然,您可能想给他们一些额外的参数(这通常很有用,这取决于您)。
static void my_internal_foo1(struct my_foo_state_st*sta) {
int x, y;
// do something complex before that and compute x,y
if (SomeErrorConditionAbout(sta))
internal_error_printf(sta, 35 /*error code*/,
"errror: bad x=%d y=%d", x, y);
// otherwise do something complex after that, and mutate sta
}
static void my_internal_foo2(struct my_foo_state_st*sta) {
// do something complex
if (SomeConditionAbout(sta))
my_internal_foo1(sta);
// do something complex and/or mutate or use `sta`
}
(即使你有几十个像上面这样的内部函数,你也不会jmp_buf
在其中任何一个函数中使用 a ;你也可以在它们中进行非常深入的递归。你只需要在所有函数中传递一个-to指针struct my_foo_state_st
,如果你是单线程的并且不关心可重入性,您可以将该指针存储在某个static
变量中...或某个线程本地变量中,甚至无需将其传递给某个参数,我认为这仍然更可取-因为更多的可重入和线程友好)。
最后,这是公共功能:它设置状态并执行setjmp
// the public function
const char* my_foo (struct some_arg_st* arg) {
struct my_state_st sta;
memset(&sta, 0, sizeof(sta));
int err = setjmp(sta->jb);
if (!err) { // first call
/// put something in `sta` related to ̀ arg̀
/// start the internal processing
//// later,
my_internal_foo1(&sta);
/// and other internal functions, possibly recursive ones
/// we return NULL to tell the caller that all is ok
return NULL;
}
else { // error recovery
/// possibly release internal consumed resources
return sta->rs;
};
abort(); // this should never be reached
}
请注意,您可以调用您my_foo
的十亿次,它不会在没有失败时消耗任何堆内存,并且堆栈将增长一百字节(在从返回之前释放my_foo
)。即使您的私有代码调用十亿次失败了十亿次,如果编码正确,也internal_error_printf
不会发生内存泄漏(因为您记录了返回调用者应该my_foo
返回的错误字符串)。free
因此,正确使用十亿次并不会占用大量内存( setjmp
longjmp
单个local的调用堆栈上只有几百字节,在函数返回时jmp_buf
弹出)。my_foo
确实,longjmp
它比普通的成本略高return
(但它会进行没有的转义return
),因此您更愿意在错误情况下使用它。
但是使用setjmp
andlongjmp
很棘手,但高效且可移植,并且使您的代码难以理解,如setjmp 所述。重要的是要非常认真地评论它。setjmp
巧妙而明智地使用这些longjmp
并不需要“千兆字节”的 RAM,正如已编辑的问题中错误地说的那样(因为您在调用堆栈上只消耗一个jmp_buf
,而不是数十亿个)。如果您想要更复杂的控制流,您将jmp_buf
在调用堆栈中的每个动态“捕获点”使用本地(您可能会有几十个,而不是数十亿个)。你需要数百万jmp_buf
仅在递归数百万调用帧的假设情况下,每个调用帧都是一个捕获点,这是不现实的(即使没有任何异常处理,您也永远不会有一百万深度的递归)。
setjmp
有关C 中的“异常”处理(以及其他的 SFTW)的更好解释,请参阅此内容。FWIW,Chicken Scheme非常有创意地使用longjmp
和setjmp
(与垃圾收集和call/cc
!)
备择方案
setcontext(3)可能是 POSIX,但现在已经过时了。
GCC有几个有用的扩展(其中一些被Clang/LLVM理解):语句表达式、本地标签、标签作为值和计算的 goto、嵌套函数、构造函数调用等。
(我的感觉是你误解了一些概念,特别是调用堆栈的确切作用,所以你的问题很不清楚;我提供了一些有用的参考)
返回一个小 struct
另请注意,在某些ABI上,特别是 Linux 上的 x86-64 ABI,返回一个小 struct
的(例如两个指针,或一个指针和一个int
或long
或intptr_t
数字)非常有效(因为指针或整数都通过寄存器),并且您可以利用这一点:决定您的函数返回一个指向主要结果的指针和一些错误代码,两者都打包在一个 small 中struct
:
struct tworesult_st {
void* ptr;
int err;
};
struct towresult_st myfunction (int foo) {
void* res = NULL;
int errcode = 0;
/// do something
if (errcode)
return (struct tworesult_st){NULL, errcode};
else
return (struct tworesult_st){res, 0};
}
在 Linux/x86-64 上,上面的代码经过优化(使用 编译时gcc -Wall -O
)以在两个寄存器中返回(返回的 不消耗任何堆栈struct
)。
使用这样的函数非常简单且非常高效(不涉及内存,两个成员 ̀ struct 将在处理器寄存器中传递)并且可以简单如下:
struct tworesult_st r = myfunction(34);
if (r.err)
{ fprintf(stderr, "myfunction failed %d\n", r.err); exit(EXIT_FAILURE); }
else return r.ptr;
当然你可以有一些更好的错误处理(这取决于你)。
其他提示
阅读有关语义的更多信息,尤其是操作语义。
如果可移植性不是主要问题,请研究系统的调用约定及其 ABI 和生成的汇编代码(gcc -O -Wall -fverbose-asm foo.c
然后查看内部foo.s
),并对相关asm
指令进行编码。
也许libffi可能是相关的(但我仍然不明白你的目标,只是猜到了)。
您可以尝试使用标签 exprs 和计算的 goto,但除非您了解生成的汇编代码,否则结果可能不是您所期望的(因为堆栈指针在函数调用和返回时发生变化)。
自修改代码是不受欢迎的(在标准C99中是“不可能的” ),大多数 C 实现将二进制代码放在只读代码段中。另请阅读蹦床功能。考虑一下JIT 编译技术,比如 libjit、asmjit、GCCJIT。
(我坚信,对您的担忧的务实答案是longjmp
使用合适的编码约定,或者简单地返回一个小的struct
;两者都可以以非常有效的方式便携使用,我无法想象它们效率不够高的情况)
一些语言:Schemecall/cc
及其回溯功能,Prolog 及其回溯功能,可能更适应(比 C99)OP 的需求。