2011-03-17 72 views
1

我正在嘗試創建一個使用C#的CSharpCodeProvider在運行時生成C#代碼的腳本系統。這是一個我正在編寫的簡單的遊戲引擎,用XNA 4.0編寫。 目標是讓用戶能夠通過C#修改遊戲元素,而無需訪問遊戲引擎的任何細節(渲染代碼,物理代碼,網絡等)。腳本在運行時由引擎編譯爲DLL。目前,我在引擎和已編譯的腳本DLL之間建立了通信。 (我創建了一個Player.cs腳本,並且在編譯之後,它能夠從腳本DLL中調用我的引擎的「Engine.Print(」Foobar「);方法)引擎還能夠使用腳本的方法(引擎遍歷在編譯後的腳本所定義的所有新類,並調用編譯後的「OnCompile()」方法TargetInvocationException與CSharpCodeProvider生成的DLL

問題與腳本間通信的開始:我有2個腳本,庫存和播放器:

Inventory.cs :

public class Inventory 
{ 
    int foobar; 

    public Inventory() 
    { 
     foobar = 42; 
    } 

    public static void OnCompile() 
    { 
     // This method exists in the Engine DLL, linked to this script 
     Engine.Print("OnCompile Inventory");  
    } 
} 

Player.cs:

using Scripts.Inventory; 

public class Player 
{ 
    Inventory inventory; 

    public Player() 
    { 
     //inventory = new Inventory(); 
     Engine.Print("Player created"); 
    } 


    public static void OnCompile() 
    { 
     Engine.Print("OnCompile Player"); 
     Player test = new Player(); 
    } 
} 

此代碼的功能,調試輸出打印:

OnCompile庫存
OnCompile播放
Player創建

然而,一旦我去掉庫存=新庫存();在播放器的構造 調試輸出如下:

OnCompile庫存
OnCompile球員

未處理的異常:System.Reflection.TargetInvocationException:異常已通過調用的目標引發異常。 ---> System.IO.FileNotFoundException:無法加載文件或程序集'Inventory.cs,Version = 0.0.0.0,Culture = neutral,PublicKeyToken = null'或其依賴項之一。該系統找不到指定的文件。 () at Scripts.Player.Player.ctor() at Scripts.Player.Player.OnCompile()

我已經確保我的Player.cs.dll引用了Inventory.cs.dll。我的彙編代碼如下:

public static bool Compile(string fileName, bool forceRecompile = false) 
    { 
     // Check to see if this assembly already exists. 
     // If it does, then just return a reference to it, unless 
     // it is told to forceRecompile, in which case 
     // it will delete the old, and continue compiling 
     if (File.Exists("./" + fileName + ".dll")) 
     { 
      if (forceRecompile) 
      { 
       File.Delete(fileName + ".dll"); 
      } 
      else 
      { 
       return true; 
      } 
     } 


     // Generate a name space name. this means removing the initial ./ 
     // of the path, and replacing all subsequent /'s with .'s 
     // Also removing the .cs at the end 

     // i.e: ./Scripts/Player.cs becomes 
     //  Scripts.Player 

     string namespaceName = ""; 

     if (fileName.LastIndexOf('.') != -1) 
     { 
      fileName = fileName.Remove(fileName.LastIndexOf('.')); 
     } 

     namespaceName = fileName.Replace('/', '.'); 
     namespaceName = namespaceName.Substring(2); 

     // Add references, starting with ScriptBase.dll. 
     // ScriptBase.dll is a helper library that provides 
     // access to debug functions such as Console.Write 

     List<string> references = new List<string>() 
     { 
      "./ScriptBase.dll", 
      "System.dll"  // TODO: remove later 
     }; 


     // Open the script file wit ha StreamReader 
     StreamReader fileStream; 
     string scriptSource = ""; 

     fileStream = File.OpenText("./" + fileName + ".cs"); 


     // Preprocess the script. This is important, as it resolves 
     // using statements, so that if a script references another 
     // script, it will have the dependency registered before 
     // compiling. 
     do 
     { 
      string line = fileStream.ReadLine(); 

      string[] words = line.Split(' '); 

      // Found a using statement: 
      if (words[0] == "using") 
      { 
       // Get the namepsace name: 
       string library = words[1]; 

       library = library.Remove(library.Length - 1); // get rid of semicolon 

       // Convert back to a path 
       library = library.Replace('.', '/'); 


       // See if the assembly exists, or we are forcing the recompilation 
       if (!File.Exists("./" + library + ".cs.dll") || forceRecompile) 
       { 
        // We need to compile it now. 
        // See if the script file exists... 
        if (File.Exists("./" + library + ".cs")) 
        { 
         // if it does, compile that, if it doesn't then we bail 
         if (!Compile("./" + library + ".cs", forceRecompile)) 
         { 
          return false; 
         } 
        } 
        else 
        { 
         return false; 
        } 
       } 
       // Now that it's compiled, and we need it link it with our reference list... 
       references.Add("./" + library + ".cs.dll"); 
      } 
      // Piece it back together as one string, line by line. 
      scriptSource = scriptSource + line + "\n"; 

     } while (!fileStream.EndOfStream); 


     fileStream.Close(); 

     // Automagically add our namepsace to the script, so the scriptor doesn't have to, also automatically 
     // include ScriptBase 
     // This is where Engine class is found for Print() debug method   

     string source = "using ScriptBase; namespace " + namespaceName + "{" + scriptSource + "}"; 


     // Set up the compiler: 
     Dictionary<string, string> providerOptions = new Dictionary<string, string> 
     { 
      { "CompilerVersion", "v3.5" } 
     }; 

     CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions); 


     // Create compilation params... Here we link our references, and append ".cs.dll" to our file name 
     // So now for example, ./Scripts/Player.cs compiles to ./Script/Player.cs.dll 
     CompilerParameters compilerParams = new CompilerParameters(references.ToArray(), fileName + ".cs.dll") 
     { 
      GenerateInMemory = true, 
      GenerateExecutable = false, // compile as DLL 

     }; 

     // Compile and check errors 
     CompilerResults results = provider.CompileAssemblyFromSource(compilerParams, source); 
     if (results.Errors.Count != 0) 
     { 
      foreach (CompilerError error in results.Errors) 
      { 
       // Write out any errors found: 
       Console.WriteLine("Syntax Error in " + error.FileName + " (" + error.Line + "): " + error.ErrorText); 
      } 

      return false; 
     } 

     // Return our Script struct, which keeps all the information together, 
     // and registers it so that Script.GetCompiledScript("./Scripts/Player.cs.dll"); 
     // returns the compiled script, or null if it's never been compiled 

     Assembly.LoadFrom(fileName + ".cs.dll"); 

     foreach (Type type in results.CompiledAssembly.GetTypes()) 
     { 
      new ScriptClass(fileName + ".cs.dll", type); 
     } 

     return true; 
    } 
} 

我已經通過代碼加強,並Inventory.cs按預期始終得到Player.cs之前編制,並正確添加到Inventory.cs.dll引用列表編譯前的Player.cs。

我必須缺少一些東西,只是簡單地鏈接引用列表中的DLL似乎不夠用,錯誤提到沒有找到文件Inventory.cs。我在哪裏指定搜索源.cs的路徑? (.cs.dll編譯的腳本始終位於與.cs源代碼腳本相同的路徑中)

回答

1

您將需要爲AppDomain.AssemblyResolve事件添加處理程序。在處理程序中,您可以將程序集名稱映射到您用Assembly.LoadFrom()加載的程序集。

加載了Assembly.LoadFrom()的程序集屬於所謂的loadfrom上下文。用Assembly.Load()加載的正常引用程序集和程序集屬於加載上下文。程序集不會找到其他上下文中存在的自動引用的程序集。

在這種情況下,兩個動態編譯的程序集都會從上下文中加載到load中,因此它們也應該可以彼此對齊。但是,執行程序集存在於加載上下文中,因此無法在加載上下文中看到其他程序集。 results.CompiledAssembly.GetTypes()強制將程序集加載到加載上下文中,並拋出異常,因爲程序集引用無法解析。需要使用AppDomain.AssemblyResolve事件來綁定來自另一個綁定上下文的程序集。

關於結合上下文的更多信息:http://blogs.msdn.com/b/suzcook/archive/2003/05/29/57143.aspx