Iva*_*anB -25 c gcc return memory-address gcc-extensions
是否可以在GCC中做这样的事情?
void foo() {
if (something()) returnSomewhereElse;
else return;
}
void bar() {
foo();
return; // something failed, no point of continuing
somewhereElse:
// execution resumes here if something succeeds
// ...
}
Run Code Online (Sandbox Code Playgroud)
只是为了澄清意图 - 它是关于错误处理.这个例子是一个最小的例子,只是为了说明事情.我打算在更深层次的上下文中使用它,以便在发生错误时停止执行.我还假设状态没有改变,我可能错了,因为在两个返回点之间没有添加额外的局部变量,所以我希望编译器生成的代码在foo的返回时可以重用为此节省了使用longjmp
,设置和传递跳转缓冲区的开销.
这个例子"确实有意义",因为它的目的是展示我想要实现的目标,而不是为什么以及如何在实际代码中有意义.
为什么你的想法更简单,只需从foo()返回一个值并让bar()返回或执行somewhereElse:conditionally?
它并不简单,你所建议的不适用于实践,只是在一个简单的例子的背景下,但它更好,因为:
1 - 它不涉及额外返回值
2 - 它不涉及额外检查值
3 - 它不涉及额外的跳跃
我可能错误地认为目标应该在这一点上清楚,并且在所有澄清和解释之后.我们的想法是从深度调用链中提供"转义代码路径",而无需任何额外开销.通过重用编译器生成的代码来恢复前一个调用帧的状态,并简单地修改函数返回后执行恢复的指令.成功跳过"转义代码路径",发生的第一个错误进入它.
if (failure) return; // right into the escape code path
else {
doMagickHere(); // to skip the escape code path
return; // skip over the escape code path
}
//...
void bar() {
some locals;
foo();
// enter escape code path here on foo failure so
destroy(&locals); // cleanup
return; // and we are done
skipEscapeCodePath: // resume on foo success
// escape path was skipped so locals are still valid
}
Run Code Online (Sandbox Code Playgroud)
至于Basile Starynkevitch提出longjmp
的"有效"和"甚至十亿longjmp仍然合理"的说法 - sizeof(jmp_buf)
给了我大约156个字节,这显然是保存几乎所有寄存器和一堆其他东西所需的空间,所以以后可以恢复.这些都是很多操作,而这十亿次远远超出了我对"有效"和"合理"的个人理解.我的意思是十亿个跳转缓冲区本身就超过了145个GIGABYTES内存,然后还有CPU时间开销.并不是很多系统甚至可以提供那种"合理"的系统.
Bas*_*tch 39
不,这是不可能的,我不确定猜测你想要实现什么.
也许你想要一些非本地的跳跃.仔细阅读有关setjmp.h,协同程序,调用堆栈,异常处理,continuation和continuation-passing-style的内容.了解什么呼叫/ cc的是方案应该是非常有利的.
setjmp
和 longjmp
setjmp和longjmp是标准的C99函数(它们非常快,因为保存的状态实际上非常小).使用它们时要特别小心(特别是为了避免任何内存泄漏).longjmp
(或POSIX中相关的 siglongjmp)是便携式标准C99中逃避某些功能并返回某个调用者的唯一方法.
我们的想法是从深度调用链中提供"转义代码路径",而无需任何额外开销
这正是中的作用longjmp
与setjmp
.两者都是快速,恒定时间的操作(特别是longjmp
需要花费很短且恒定的时间来展开数千个调用帧的调用栈).内存开销实际上是jmp_buf
每个捕获点一个本地,不是什么大不了的事.该jmp_buf
很少把调用堆栈之外.
有效使用它们的一种常用方法是将setjmp
-ed jmp_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);
Run Code Online (Sandbox Code Playgroud)
这struct my_foo_state_st
是私人国家.这my_foo
是公共函数(您将在某些公共标题中声明).您确实记录了(至少在注释中)它在失败时返回堆分配的错误消息,因此调用者负责释放它.成功时,您记录了它返回NULL
.当然,您可以拥有其他约定和其他参数和/或结果.
我们现在声明并实现一个错误函数,它将错误消息打印到状态并使用a转义 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);
}
Run Code Online (Sandbox Code Playgroud)
我们现在有几个可能复杂的递归函数来完成大部分工作.我只画草图,你知道你想要他们做什么.当然你可能想给他们一些额外的参数(这通常是有用的,这取决于你).
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`
}
Run Code Online (Sandbox Code Playgroud)
(即使你有几十个像上述内部功能,你不消耗jmp_buf
任何人;而你也可以在他们改乘相当深刻,您只需要通过一个指针 -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
}
Run Code Online (Sandbox Code Playgroud)
请注意,您可以调用my_foo
十亿次,在没有失败时不会消耗任何堆内存,并且堆栈将增长一百个字节(在返回之前释放my_foo
).而且即使你的个人代码失败十亿次调用十亿次internal_error_printf
没有内存泄漏发生(因为你证明这my_foo
是返回该错误字符串调用者应free
)如果编码正确.
因此,使用正确 setjmp
和longjmp
十亿倍也不会吃了很多的内存(在调用堆栈上只有几百个字节为一个单一的地方jmp_buf
,这是对弹出my_foo
函数返回).实际上,longjmp
它比普通的稍微贵一点return
(但它没有逃脱return
),所以你更愿意在错误的情况下使用它.
但是使用setjmp
并且longjmp
是棘手但有效且可移植的,并且如setjmp所记录的那样使您的代码难以理解.重要的是要非常认真地评论它.使用这些并巧妙而明智地不需要"千兆字节"的RAM,正如在编辑的问题中错误地说的那样(因为你在调用堆栈上只消耗一个,而不是数十亿个).如果你想要更复杂的控制流程,你将在调用堆栈中的每个动态"捕获点" 使用本地(你可能会有几十个,而不是数十亿).在数百万个调用帧递归的假设情况下,你只需要数百万个,每个都是一个捕获点,这是不现实的(即使没有任何例外,你也永远不会有一百万的深度递归处理).setjmp
longjmp
jmp_buf
jmp_buf
jmp_buf
见这对于一个更好的解释setjmp
为"异常"用C处理(和SFTW其他的).FWIW,鸡方案有一个非常有创造力的使用longjmp
和setjmp
(与垃圾收集和call/cc
!)
setcontext(3)可能是POSIX,但现在已经过时了.
GCC有几个有用的扩展(其中一些是Clang/LLVM理解的):语句exprs,本地标签,标签作为值和计算goto,嵌套函数,构造函数调用等.
(我的感觉是你误解了一些概念,特别是调用堆栈的确切作用,所以你的问题很不清楚 ;我给了一些有用的参考资料)
struct
还要注意的是一些ABI S,尤其是X86-64 ABI在Linux上,返回一个小的 struct
(例如,两个指针,或者一个指针和一个int
或long
或intptr_t
数量)是非常有效的(因为这两个指针或整数走通寄存器),和你可以利用这个:决定你的函数返回一个指向主要结果的指针和一些错误代码,两者都打包在一个小的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};
}
Run Code Online (Sandbox Code Playgroud)
在Linux/x86-64上,上面的代码经过优化(编译时gcc -Wall -O
),返回两个寄存器(没有为返回的代码消耗任何堆栈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;
Run Code Online (Sandbox Code Playgroud)
当然,你可以有一些更好的错误处理(这取决于你).
阅读有关语义的更多信息,特别是操作语义.
如果可移植性不是主要问题,请研究系统及其ABI 的调用约定和生成的汇编代码(gcc -O -Wall -fverbose-asm foo.c
然后查看内部foo.s
),并编写相关asm
指令.
也许libffi可能是相关的(但我仍然不理解你的目标,只是猜测它们).
您可以尝试使用标签exprs和计算的gotos,但除非您了解生成的汇编代码,否则结果可能不是您所期望的(因为堆栈指针在函数调用和返回时更改).
自修改代码不受欢迎(并且在标准 C99中"不可能" ),并且大多数C实现将二进制代码放在只读代码段中.另请阅读有关蹦床功能的内容.考虑可能是JIT编译技术,即libjit,asmjit,GCCJIT.
(我坚信实用主义回答您的问题或者是longjmp
有合适的编码约定,或者简单地返回小struct
;既能够方便地在一个非常有效的方式使用,并且他们没有足够有效的,我无法想象的情况下)
有些语言:带有call/cc
Prolog及其回溯功能的Scheme 可能比OP的需要更适应(比C99更多).