C#中的setjmp/longjmp.可能吗?

Max*_*Max 3 c#

当我需要在本地范围内进行转到时,我遇到了一个问题:

if(...)      
{
   DoSomethingHere();
   if (...) goto Label;
}
else if(...)
{
Label:
  DoSomethingHereToo();
}
Run Code Online (Sandbox Code Playgroud)

,显然在C#中是不可能的.

是的我知道使用goto被认为是一种不好的做法,但在这种情况下使用goto更容易.因此,我宁愿不进入整个"goto是所有邪恶的来源"的讨论.对我来说,一个更有趣,更普遍的问题是C#中setjmp/longjmp的可能性.那么可能吗?

Eri*_*ert 12

首先,我认为你在本地范围内做一个"转到" - 一个短暂的跳跃 - 跳远 - 在当前方法之外完全转到某个地方.可以通过两种方式考虑经典的C风格跳远:一,它就像抛出一个不清理堆栈帧的异常.二,就像从函数返回到"错误"的地址.

在C#中,以上都不可能.C#不支持跳远; 我们尝试抓住最终投掷,以干净,结构化和安全的方式进行非本地游戏.

C#也不支持从局部变量声明空间外部到空间内的短跳转.原因是因为从外面跳到一个街区的中间是令人困惑,危险,难以理解和难以维护.完成此设计目标的方法是使标签与局部变量具有相同的范围."goto"甚至看不到标签,只是该位置的代码会看到在不同的局部变量声明空间中声明的局部变量.

在没有使用任何goto语句的情况下,有很多方法可以解决您的问题.例如,立即想到的是

bool doFirstThing = false;
bool doSecondThing = false;
if (firstCondition) 
{
    doFirstThing = true;
    doSecondThing = true;
}
else if (secondCondition)
{
    doSecondThing = true;
}
if (doFirstThing) 
{
    DoFirstThing();
}
if (doSecondThing)
{
    DoSecondThing();
}
Run Code Online (Sandbox Code Playgroud)

这非常简单,易于阅读,易于调试等等.

或者:如果"doSecondThing"结果中的共享代码实际上难以重构为自己的方法,那么请考虑退一步并确定您的控制流是否过于复杂而无法开始.例如,如果您在循环中改变了大量变量,那么可能有一些技术可以用来简化这种情况并减少突变.您能否提供有关此代码正在做什么以及为何难以重构的更多信息?

  • @Max:听起来你的代码非常复杂; 我会考虑退后一步,看看是否有一些更大的重构你可以执行,这将使逻辑更容易遵循.例如,如果您确实需要状态机,那么您可以通过编写一个库来简化代码,该库允许您更轻松地定义状态机,然后通过从库中构建适当的状态机来定义控制流. (2认同)

Ill*_*ack 5

这里有龙。

\n\n

为了回答标题中的问题,我的第一次尝试自然是通过互操作并从 msvcrt.dll 导入setjmplongjmp

\n\n
[DllImport("msvcrt.dll", CallingConvention=CallingConvention.Cdecl, EntryPoint="_setjmp")]\nstatic extern int setjmp(out Jmp_buf env);\n\n[DllImport("msvcrt.dll", CallingConvention=CallingConvention.Cdecl)]\nstatic extern void longjmp(ref Jmp_buf env, int val);\n\n[StructLayout(LayoutKind.Sequential,Size=16*4)]\nstruct Jmp_buf{}\n
Run Code Online (Sandbox Code Playgroud)\n\n

看来我已经使导入签名正确,但最终,它不能以这种方式工作。setjmpP/Invoke 在对本机 setjmp 的调用周围创建一个包装器,因此当 P/Invoke 方法返回时,堆栈帧已被释放。longjmp那么抛出AccessViolationException也就不足为奇了。

\n\n

就是这样。没有办法进入 mounta...我的意思是,在纯 C# 中调用这两个函数。我能想到的绝对唯一的方法是通过破解某些方法的 JITted 本机代码来“内联”调用,并手动包含对setjmp的调用。抱歉,即使我认为没有任何严肃的理由就没有理由尝试这样做。

\n\n

但如果我不能从 C# 调用该函数,我当然可以从 C++/CLI 调用!

\n\n
#include <csetjmp>\n#include <iostream>\nusing namespace System;\nusing namespace System::Runtime::InteropServices;\nusing namespace std;\n\ntypedef void (*UnmanagedHandler)(int code);\n\nvoid mysetjmp(jmp_buf env, UnmanagedHandler handler)\n{\n    handler(setjmp(env));\n    throw 0;\n}\n\nvoid mylongjmp(jmp_buf env, int val)\n{\n    longjmp(env, val);\n}\n\nnamespace jmptestdll\n{\n    public delegate void JumpHandler(int code);\n\n    public ref class JumpBuffer\n    {\n    private:\n        jmp_buf *env;\n\n    public:\n        JumpBuffer()\n        {\n            env = new jmp_buf[1];\n        }\n\n        ~JumpBuffer()\n        {\n            this->!JumpBuffer();\n        }\n\n        void Set(JumpHandler^ handler)\n        {\n            if(env)\n            {\n                IntPtr ptr = Marshal::GetFunctionPointerForDelegate(handler);\n                UnmanagedHandler act = static_cast<UnmanagedHandler>(ptr.ToPointer());\n                try{\n                    mysetjmp(*env, act);\n                }catch(int code)\n                {\n\n                }\n            }\n        }\n\n        void Jump(int value)\n        {\n            if(env)\n            {\n                mylongjmp(*env, value);\n            }\n        }\n\n    protected:\n        !JumpBuffer()\n        {\n            if(env)\n            {\n                delete[] env;\n            }\n        }\n    };\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我可能在该代码中犯了一些可怕的错误,但 C++ 不是我的母语,抱歉。不过,它确实有效。由于某种原因,从mysetjmpthrows AccessViolationException返回也会抛出 AccessViolationException,但我还没有找到原因。通过作品“回归” throw

\n\n
var env = new JumpBuffer();\nenv.Set(\n    delegate(int code)\n    {\n        Console.WriteLine(code);\n        env.Jump(code+1);\n        Console.WriteLine("Goodbye world!");\n    }\n);\n
Run Code Online (Sandbox Code Playgroud)\n\n

“世界再见!” 永远不会显示,而是显示从 0 开始升序的数字。移植维基百科的例子也有效:

\n\n
static JumpBuffer buf = new JumpBuffer();\n\nstatic void second()\n{\n    Console.WriteLine("second");\n    try{\n        buf.Jump(1);\n    }finally{\n        Console.WriteLine("finally");\n    }\n}\n\nstatic void first()\n{\n    second();\n    Console.WriteLine("first");\n}\n\npublic static void Main(string[] args)\n{\n    buf.Set(\n        val => {\n            Console.WriteLine(val);\n            if(val == 0) first();\n            else Console.WriteLine("main");\n        }\n    );\n\n    Console.ReadKey(true);\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

输出:

\n\n
\n

0
\n 第二
\n 最后
\n 1
\n 主要

\n
\n\n

起初我以为它也会跳过finally处理程序,但我想这并没有那么邪恶。唯一的缺点是我们不能直接编写代码Set,而必须传递处理程序。

\n\n

如果您想跳转到任意方法到任意标签,请创建一个状态机。

\n\n

嗯,C# 中有内部状态机支持迭代器和async. 迭代器对于我们的目的来说太有限了,但是有了await,它可能正是我们所需要的。

\n\n
public class LongJump\n{\n    Continuation continuation;\n\n    public SetAwaiter Set()\n    {\n        return new SetAwaiter(this);\n    }\n\n    public JumpAwaiter Jump()\n    {\n        return new JumpAwaiter(this);\n    }\n\n    public struct JumpAwaiter : INotifyCompletion\n    {\n        readonly LongJump jump;\n\n        public JumpAwaiter(LongJump jump)\n        {\n            this.jump = jump;\n        }\n\n        public JumpAwaiter GetAwaiter()\n        {\n            return this;\n        }\n\n        public bool IsCompleted{\n            get{\n                return false;\n            }\n        }\n\n        public void OnCompleted(Action callerContinuation)\n        {\n            jump.continuation.Continue();\n        }\n\n        public void GetResult()\n        {\n\n        }\n    }\n\n    public struct SetAwaiter : INotifyCompletion\n    {\n        readonly LongJump jump;\n\n        public SetAwaiter(LongJump jump)\n        {\n            this.jump = jump;\n        }\n\n        public SetAwaiter GetAwaiter()\n        {\n            return this;\n        }\n\n        public bool IsCompleted{\n            get{\n                return false;\n            }\n        }\n\n        public void OnCompleted(Action callerContinuation)\n        {\n            jump.continuation = new Continuation(callerContinuation);\n            callerContinuation();\n        }\n\n        public void GetResult()\n        {\n\n        }\n    }\n\n    private class Continuation\n    {\n        private readonly int savedState;\n        private readonly object stateMachine;\n        private readonly FieldInfo field;\n        private readonly Action action;\n\n        internal Continuation(Action action)\n        {\n            stateMachine = action.Target.GetType().InvokeMember("m_stateMachine", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetField, null, action.Target, null);\n            field = stateMachine.GetType().GetField("<>1__state", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);\n            savedState = (int)field.GetValue(stateMachine);\n            this.action = action;\n        }\n\n        internal void Continue()\n        {\n            field.SetValue(stateMachine, savedState);\n            action();\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

\xc2\xa0

\n\n
public static void Main(string[] args)\n{\n    MainAsync().Wait();\n\n    Console.ReadKey(true);\n}\n\npublic static async Task MainAsync()\n{\n    var jump = new LongJump();\n    Console.WriteLine("Begin");\n    int code = 0;\n    await jump.Set();\n    Console.WriteLine(code);\n    code += 1;\n    await InnerMethod(code, jump);\n    Console.WriteLine("End");\n}\n\npublic static async Task InnerMethod(int code, LongJump jump)\n{\n    if(code < 5)\n    {\n        await jump.Jump();\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我从 Jon Skeet 关于用 C# 实现 COMEFROM 的精彩文章中得到了一些灵​​感。

\n\n

总结一下这段代码,callingawait jump.Set();实际上是记住了那一刻状态机的状态,然后照常继续执行。await jump.Jump();放弃该行之后的任何延续,并恢复旧的延续。您甚至可以跳转到已经结束的方法,但不要从该方法返回,因为它会尝试再次标记其任务已完成,从而导致异常。

\n\n

我想也可以将异步方法与 C++/CLI 代码结合起来,以await从 中删除jump.Jump(),但这不太有用。

\n\n

请记住,C# 中已经有一个有用的“长跳转”机制 - 异常处理。

\n