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

时间:2010-11-15 15:00:26

标签: c#

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

if(...)      
{
   DoSomethingHere();
   if (...) goto Label;
}
else if(...)
{
Label:
  DoSomethingHereToo();
}

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

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

9 个答案:

答案 0 :(得分: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();
}

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

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

答案 1 :(得分:3)

不要做下面描述的任何事情。这是一个坏主意,仅用于提供信息或作为智力练习。


C#语言不支持跳出范围。所以,你不可能在C#中做你想要的。但是, IL 将允许您这样做,因为IL的级别低于C#,并且实际上没有这种形式的范围。

所以,如果你真的想在忽略范围的同时支持goto,你可以使用后编译工具来伪造它来调整IL。与Mike Stall's tool类似的东西。注意:这是一个可怕的,可怕的想法。我甚至认为这是一种智力锻炼,我感到很遗憾。

如果您真的尝试为生产代码执行此操作,这将会中断:

  • C#编译器将忽略您的特殊代码,因此它可能会以“安全”方式重新排列您的代码,而不会考虑您的非本地goto。
  • 编写C#编译器很难。创建一个后处理器以将随机IL嵌入到您的代码中并不是太困难(特别是以Mike Stall的工具为起点),但以可靠的方式执行它非常困难。
  • C#因某种原因不支持非本地goto;使用非本地goto时,编写损坏的代码非常容易。更糟糕的是,如果你对C#编译器进行最终运行并尝试强制它自己工作。

答案 2 :(得分:3)

这里是龙。

尝试回答标题中的问题,我第一次尝试完成它,当然是通过互操作并从msvcrt.dll导入 setjmp longjmp

[DllImport("msvcrt.dll", CallingConvention=CallingConvention.Cdecl, EntryPoint="_setjmp")]
static extern int setjmp(out Jmp_buf env);

[DllImport("msvcrt.dll", CallingConvention=CallingConvention.Cdecl)]
static extern void longjmp(ref Jmp_buf env, int val);

[StructLayout(LayoutKind.Sequential,Size=16*4)]
struct Jmp_buf{}

看来我已经使导入签名正确但最终,它无法以这种方式工作。 P / Invoke围绕对本机setjmp的调用创建一个包装器,因此当P / Invoke方法返回时,setjmp的堆栈帧已经被释放。毫无疑问longjmp会抛出 AccessViolationException

就是这样。在mounta中有无法 ...我的意思是,在纯C#中调用这两个函数。我能想到的绝对唯一的方法是通过黑客攻击某些方法的JITted本机代码来“内联”调用,并手动包含对 setjmp 的调用。对不起,即使我没有理由在没有任何严重理由的情况下尝试这样做。

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

#include <csetjmp>
#include <iostream>
using namespace System;
using namespace System::Runtime::InteropServices;
using namespace std;

typedef void (*UnmanagedHandler)(int code);

void mysetjmp(jmp_buf env, UnmanagedHandler handler)
{
    handler(setjmp(env));
    throw 0;
}

void mylongjmp(jmp_buf env, int val)
{
    longjmp(env, val);
}

namespace jmptestdll
{
    public delegate void JumpHandler(int code);

    public ref class JumpBuffer
    {
    private:
        jmp_buf *env;

    public:
        JumpBuffer()
        {
            env = new jmp_buf[1];
        }

        ~JumpBuffer()
        {
            this->!JumpBuffer();
        }

        void Set(JumpHandler^ handler)
        {
            if(env)
            {
                IntPtr ptr = Marshal::GetFunctionPointerForDelegate(handler);
                UnmanagedHandler act = static_cast<UnmanagedHandler>(ptr.ToPointer());
                try{
                    mysetjmp(*env, act);
                }catch(int code)
                {

                }
            }
        }

        void Jump(int value)
        {
            if(env)
            {
                mylongjmp(*env, value);
            }
        }

    protected:
        !JumpBuffer()
        {
            if(env)
            {
                delete[] env;
            }
        }
    };
}

我可能在该代码中犯了一些可怕的错误,但是C ++不是我的母语,对不起。但它确实可以解决问题。出于某种原因,从mysetjmp返回也会引发 AccessViolationException ,但我还没有找到原因。通过throw“返回”工作。

var env = new JumpBuffer();
env.Set(
    delegate(int code)
    {
        Console.WriteLine(code);
        env.Jump(code+1);
        Console.WriteLine("Goodbye world!");
    }
);

“再见世界!”永远不会显示,而是显示从0开始的数字。移植Wikipedia example也有效:

static JumpBuffer buf = new JumpBuffer();

static void second()
{
    Console.WriteLine("second");
    try{
        buf.Jump(1);
    }finally{
        Console.WriteLine("finally");
    }
}

static void first()
{
    second();
    Console.WriteLine("first");
}

public static void Main(string[] args)
{
    buf.Set(
        val => {
            Console.WriteLine(val);
            if(val == 0) first();
            else Console.WriteLine("main");
        }
    );

    Console.ReadKey(true);
}

输出:

  

0
  第二
  最后
  1
  主

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

如果要将任意方法跳转到任意标签,请创建状态机。

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

public class LongJump
{
    Continuation continuation;

    public SetAwaiter Set()
    {
        return new SetAwaiter(this);
    }

    public JumpAwaiter Jump()
    {
        return new JumpAwaiter(this);
    }

    public struct JumpAwaiter : INotifyCompletion
    {
        readonly LongJump jump;

        public JumpAwaiter(LongJump jump)
        {
            this.jump = jump;
        }

        public JumpAwaiter GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted{
            get{
                return false;
            }
        }

        public void OnCompleted(Action callerContinuation)
        {
            jump.continuation.Continue();
        }

        public void GetResult()
        {

        }
    }

    public struct SetAwaiter : INotifyCompletion
    {
        readonly LongJump jump;

        public SetAwaiter(LongJump jump)
        {
            this.jump = jump;
        }

        public SetAwaiter GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted{
            get{
                return false;
            }
        }

        public void OnCompleted(Action callerContinuation)
        {
            jump.continuation = new Continuation(callerContinuation);
            callerContinuation();
        }

        public void GetResult()
        {

        }
    }

    private class Continuation
    {
        private readonly int savedState;
        private readonly object stateMachine;
        private readonly FieldInfo field;
        private readonly Action action;

        internal Continuation(Action action)
        {
            stateMachine = action.Target.GetType().InvokeMember("m_stateMachine", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetField, null, action.Target, null);
            field = stateMachine.GetType().GetField("<>1__state", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
            savedState = (int)field.GetValue(stateMachine);
            this.action = action;
        }

        internal void Continue()
        {
            field.SetValue(stateMachine, savedState);
            action();
        }
    }
}

public static void Main(string[] args)
{
    MainAsync().Wait();

    Console.ReadKey(true);
}

public static async Task MainAsync()
{
    var jump = new LongJump();
    Console.WriteLine("Begin");
    int code = 0;
    await jump.Set();
    Console.WriteLine(code);
    code += 1;
    await InnerMethod(code, jump);
    Console.WriteLine("End");
}

public static async Task InnerMethod(int code, LongJump jump)
{
    if(code < 5)
    {
        await jump.Jump();
    }
}

我得到了Jon Skeet在C#中实现COMEFROM的精彩article的灵感。

总结一下这段代码,调用await jump.Set();实际上会记住当时状态机的状态,然后像往常一样继续执行。 await jump.Jump();丢弃该行之后的任何延续,并恢复旧的继续。你甚至可以跳转到一个已经结束的方法,但是不要从它返回,因为它会尝试再次完成它的任务,导致异常。

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

请记住,在C#中还有一个有用的“跳远”机制 - 异常处理。

答案 3 :(得分:1)

为什么不会:

condition1Cache = condition1;
condition2Cache = false;
if ( condition1Cache )
{    
   yadda yadda
   condition2Cache = condition2;
}
/* short-circuit evaluation will prevent condition3 from being evaluated (and possibly having side-effects) in a manner compatible with the original code. */
if ( ( condition1Cache && condition2Cache ) || (!condition1Cache && condition3) ) 
{
   bada bing
}

工作?

编辑:已更新以使用缓存,以避免在您不希望出现副作用时导致副作用的情况。

答案 4 :(得分:1)

如果条件和结果代码可以表示为rvalues,则可以使用短路操作来执行在不使用gotos或flags的情况下无法实现的操作。在你的情况下:

if (condition1() ? (DoSomethingHere(),condition2()) : condition3())
  DoSomethingHere2();

除非DoSomethingHere与condition2的评估明确相关,否则我通常不会如何对其进行编码,但它应该产生所需的语义。我不确定我是否希望编译器能够识别?:对条件的影响(不同于将其评估为零/非零结果,并基于此进行条件跳转)。

顺便说一句,我倾向于讨厌标志的某些用法,而不是讨厌我,因为每个标志都为程序流添加了另一个“维度” - 如果正在执行程序流程图,可能是每个不同的标志组合在程序中的任何给定位置处相关表示不同的节点。如果使用'goto'并且没有标志可以实现必要的执行模式,那么这可能比标志更好,而不是'goto'。

答案 5 :(得分:0)

根据您的代码,以下内容是等效的:

if(condition1)      
{
   DoSomethingHere();
}

if(condition2)
{
   DoSomethingHereToo();
}

你能提供更多的背景来说明为什么这不起作用吗?

答案 6 :(得分:0)

这是一种方式,我不确定它是否是最好的,在没有goto的情况下做你想做的事情

bool doSomthingElseFlag = false
if(...)      
{
   DoSomethingHere();
   if (...)
      doSomthingElseFlag = true;
}
else if(...)
{
  DoSomethingHere2();
  if (...)
     doSomthingElseFlag = true;
}
else if(...)
{
  //Not This function does not need the other function to run
  //so we do not set the flag
  DoSomethingHere3();
}
if (doSomthingElseFlag)
{
  DoSomethingElse();
}

答案 7 :(得分:0)

关于setjmp / longjmp的主题,你可以使用它的更大的兄弟:continuations。 C#不支持使用该语言,但您可以使用continuation passing style,结合lazy evaluationtrampolines,这样就无法获得stack overflow http://sstatic.net/stackoverflow/img/favicon.ico

答案 8 :(得分:-1)

是的,这是可能的。考虑一下:

   void method1()
    {
       for (var i = 0; i < 100; i++)
       {
           method2(i);
           Console.WriteLine(i);

            EndOfLoop: //This is something like a setjmp marker
                    ;
         }
    }

    void method2(int i)
    {
       if (i%10 == 0)
           Console.WriteLine("Next Number can be divided by 10");

   // Now Long jmp to EndOfLoop
    #if IL
        br EndOfLoop 
    #endif
    }

然而,它很快就会使您的代码无法读取:)

代码的输出是:

1 2 3 4 五 6 7 8 9 下一个数字可以除以10 10 11 .....每个都在一个新的行中。

方法1的ILDASM输出:

.method private hidebysig static void  method1() cil managed
{
  // Code size       35 (0x23)
  .maxstack  2
  .locals init ([0] int32 i,
           [1] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  ldc.i4.0
  IL_0002:  stloc.0
  IL_0003:  br.s       IL_0019
  IL_0005:  nop
  IL_0006:  ldloc.0
  IL_0007:  call       void Test::method2(int32)
  IL_000c:  nop
  IL_000d:  ldloc.0
  IL_000e:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0013:  nop
  IL_0014:  nop
  IL_0015:  ldloc.0
  IL_0016:  ldc.i4.1
  IL_0017:  add
  IL_0018:  stloc.0
  IL_0019:  ldloc.0
  IL_001a:  ldc.i4.s   100
  IL_001c:  clt
  IL_001e:  stloc.1
  IL_001f:  ldloc.1
  IL_0020:  brtrue.s   IL_0005
  IL_0022:  ret
} // end of method Test::method1

方法2的ILDASM输出:

.method private hidebysig static void  method2(int32 i) cil managed
{
  // Code size       27 (0x1b)
  .maxstack  2
  .locals init ([0] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldc.i4.s   10
  IL_0004:  rem
  IL_0005:  ldc.i4.0
  IL_0006:  ceq
  IL_0008:  ldc.i4.0
  IL_0009:  ceq
  IL_000b:  stloc.0
  IL_000c:  ldloc.0
  IL_000d:  brtrue.s   IL_001a
  IL_000f:  ldstr      "Next Number can be divided by 10"
  IL_0014:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0019:  nop
  IL_001a:  ret
} // end of method Test::method2

执行输出示例:

enter image description here