封装Lua for C#

http://blog.csdn.net/rcfalcon/article/details/5583095

今天翻了一下google,翻出GameDev.net上一篇老外的文章,叫《Using Lua with C#》,看了一下,它的方法不错。(改天考虑翻译这篇文章),不过他的示例代码实在是太太太冗长了,大部分是生成函数介绍和函数帮助文档等,直接忽略。把它最核心的东西拿过来,然后自己封装了一下,用起来感觉不错。

 

基本思想是,使用C#的Attribute来标记函数,实现自动绑定。

 

核心部分代码如下(LuaFramework.cs):

 

[c-sharp]  view plain copy
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Windows;  
  6. using System.Reflection;  
  7. using LuaInterface;  
  8.   
  9. namespace WPFLuaFramework  
  10. {  
  11.     /// <summary>  
  12.     /// Lua函数描述特性类  
  13.     /// </summary>  
  14.     public class LuaFunction : Attribute  
  15.     {  
  16.         private String FunctionName;  
  17.   
  18.         public LuaFunction(String strFuncName)  
  19.         {  
  20.             FunctionName = strFuncName;  
  21.         }  
  22.   
  23.         public String getFuncName()  
  24.         {  
  25.             return FunctionName;  
  26.         }  
  27.     }  
  28.   
  29.     /// <summary>  
  30.     /// Lua引擎  
  31.     /// </summary>  
  32.     class LuaFramework  
  33.     {  
  34.         private Lua pLuaVM = new Lua();//lua虚拟机  
  35.   
  36.         /// <summary>  
  37.         /// 注册lua函数  
  38.         /// </summary>  
  39.         /// <param name="pLuaAPIClass">lua函数类</param>  
  40.         public void BindLuaApiClass( Object pLuaAPIClass )  
  41.         {  
  42.             foreach (MethodInfo mInfo in pLuaAPIClass.GetType().GetMethods())  
  43.             {  
  44.                 foreach (Attribute attr in Attribute.GetCustomAttributes(mInfo))  
  45.                 {  
  46.                     string LuaFunctionName = (attr as LuaFunction).getFuncName();  
  47.                     pLuaVM.RegisterFunction(LuaFunctionName, pLuaAPIClass, mInfo);  
  48.                 }  
  49.             }  
  50.         }  
  51.   
  52.         /// <summary>  
  53.         /// 执行lua脚本文件  
  54.         /// </summary>  
  55.         /// <param name="luaFileName">脚本文件名</param>  
  56.         public void ExecuteFile(string luaFileName)  
  57.         {  
  58.             try  
  59.             {  
  60.                 pLuaVM.DoFile(luaFileName);  
  61.             }  
  62.             catch (Exception e)  
  63.             {  
  64.                 MessageBox.Show(e.ToString());  
  65.             }  
  66.         }  
  67.   
  68.         /// <summary>  
  69.         /// 执行lua脚本  
  70.         /// </summary>  
  71.         /// <param name="luaCommand">lua指令</param>  
  72.         public void ExecuteString(string luaCommand)  
  73.         {  
  74.             try  
  75.             {  
  76.                 pLuaVM.DoString(luaCommand);  
  77.             }  
  78.             catch (Exception e)  
  79.             {  
  80.                 MessageBox.Show(e.ToString());  
  81.             }  
  82.         }  
  83.     }  
  84. }  

 

我的LUA API类如下,用于实现C# for lua的函数(LuaAPI.cs)

 

[c-sharp]  view plain copy
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Windows;  
  6.   
  7. namespace WPFLuaFramework  
  8. {  
  9.     class LuaAPI  
  10.     {  
  11.         [LuaFunction("lua1")]  
  12.         public void a1()  
  13.         {  
  14.             MessageBox.Show("a1 called");  
  15.         }  
  16.   
  17.         [LuaFunction("lua2")]  
  18.         public int a2()  
  19.         {  
  20.             MessageBox.Show("a2 called");  
  21.             return 0;  
  22.         }  
  23.   
  24.         [LuaFunction("lua3")]  
  25.         public void a3(string s)  
  26.         {  
  27.             MessageBox.Show("a3 called");  
  28.         }  
  29.     }  
  30. }  

 

最后看调用代码,是不是很简单

 

[c-sharp]  view plain copy
  1. LuaFramework test = new LuaFramework();  
  2. test.BindLuaApiClass(new LuaAPI());  
  3. test.ExecuteFile("test.lua");  
  4. test.ExecuteString("lua1()");  

 

LUA代码如下

 

lua1();
lua2();
lua3("test");

------------------------------------------------------------------------------------

原文:

http://www.gamedev.net/page/resources/_/technical/game-programming/using-lua-with-c-r2275

When developing large software projects, one of the main time consuming tasks is building the program over and over. Modern day compilers and hardware try to be smarter and faster and minimizethat as much as they possibly can, but being honest, we still spend quite a lot of time staring blankly at the screen between builds. To cope with that problem, we've seen an increasing trend ofembedding lightweight interpreted languages that allow us to easily and quickly change pieces of code and see what happens instantly, instead of waiting for another dreadful build cycle. Two of themost common choices are Python and Lua, because the way the languages are designed and implemented makes it quite easy to embed them in other applications. Also, they provide a simpler interface forthe less technically talented people to write new features without having to learn something as daunting as C++ or pester one of the programmers to code in the idea.

For this "tutorial" we're going to work with C# and the LuaInterface assembly. You can find them over at MSDN ( get the .NET 1.1framework SDK) and luaForge ( version 1.3.0 at the moment). Newer versions should work just fine as soon as they come out.

Embedding Lua into C# with LuaInterface is so easy that it's almost embarrassing. The wrapper takes care of most everything and exposes a very easy to work with API. To start with it right away,we could try this:

using System;
using LuaInterface;

namespace LuaCon
{
   class Program
   {
	  private bool bRunning = true;
	  public static Lua pLuaVM = null;

	  public Program()
	  {
		 pLuaVM = new Lua();
	  }

	  static void Main(string[] args)
	  {
		 Program pPrg = new Program();
		 pPrg.Run();
	  }

	  public void Run()
	  {
		 String strInput;

		 while (bRunning)
		 {
			Console.Write("> ");

			strInput = Console.ReadLine();
			if (strInput == "quit")
			   bRunning = false;
			else
			{
			   Console.WriteLine();
			   try
			   {
				  pLuaVM.DoString(strInput);
			   }
			  
			   catch (Exception ex)
			   {
				  Console.WriteLine(ex.Message);
			   }

			   finally
			   {
				  Console.WriteLine();
			   }
			}
		 }
	  }
   }
}
Compile it and run it, you have your very own Lua console to fool around with. Not very useful as it is, but it's a start. The next thing we'd want to do is to expose some of our own functions tothe Lua virtual machine. Again, LuaInterface comes to the rescue:  Lua.RegisterFunction(Name, Target, Method)will do just that. The parameters needed are a string, an objectinstance and a  MethodInfo instance for the method we want.

If you don't know what reflection is, a brief introduction is in order. If you do, just skip to the next paragraph. C# (like Java) has a very nifty feature: reflection. In a nutshell, it lets yourip apart any class, property or method at run time, without needing to know anything about it at build time. The  MethodInfoinstance we have to pass to the Lua VM is thereflected method taken from the object. We'll make use of it for quite a lot of things later on.

The first function we could expose to Lua would be a quit function to get rid of that horrible hack in the main loop. Our quit func could be:

public void quit()
{
   bRunning = false;
}
Amazingly simple, as you see. It just switches the running flag off. To make it available for Lua, we have to modify the main() function a bit:

static void Main(String[] args)
{
   Program pPrg = new Program();
   Type pPrgType = pPrg.GetType();
   MethodInfo mInfo = pPrgType.GetMethod("quit");
   pPrg.pLuaVM.RegisterFunction("quit", pPrg, mInfo);
   pPrg.Run();
}
Now, take out the ugly hack, the Run method looks like this:

public void Run()
{
   String strInput;

   while (bRunning)
   {
	  Console.Write("> ");

	  strInput = Console.ReadLine();
	  Console.WriteLine();
	  try
	  {
		 pLuaVM.DoString(strInput);
	  }
	  
	  catch (Exception ex)
	  {
		 Console.WriteLine(ex.Message);
	  }

	  finally
	  {
		 Console.WriteLine();
	  }
   }
}
Add  System.Reflection to the list of usages and recompile. Run and now whenever you call  quit() from the console, it will actually be Lua makingit happen. Of course, adding functions like this is a bit awkward if you end up having hundreds of them. It also requires you to touch several parts of the program if you want to add a new function,which often leads to forgetting about some part of the process. Wouldn't it be nice if we could just flag a function somehow in it's declaration and have it automagically picked up by the Lua VM?That way, we would just have to worry about writing them. To fix this, we'll have a look at one of my favourite features of C#: Attributes.

Attributes in C# are a special class (and descendants) which exist linked with other language elements. They can be attached to class declarations, method declarations, properties, local variablesor well, pretty much everything. Then, at run time, you can access them via reflection. The power of this is simply incredible, as we'll see in a moment.

First, we'll need a custom attribute class. Nothing too complicated:

public class AttrLuaFunc : Attribute
{
	private String FunctionName;
	private String FunctionDoc;
	private String[] FunctionParameters = null;

	public AttrLuaFunc(String strFuncName, String strFuncDoc, params String[] strParamDocs)
	{
		FunctionName = strFuncName;
		FunctionDoc = strFuncDoc;
		FunctionParameters = strParamDocs;
	}

	public AttrLuaFunc(String strFuncName, String strFuncDoc)
	{
		FunctionName = strFuncName;
		FunctionDoc = strFuncDoc;
	}

	public String getFuncName()
	{
		return FunctionName;
	}

	public String getFuncDoc()
	{
		return FunctionDoc;
	}

	public String[] getFuncParams()
	{
		return FunctionParameters;
	}
}
It has a function name, a function "doc" string and an array of parameter definitions. With it, the declaration of our "quit" function would look like this:

[AttrLuaFunc("quit", "Exit the program.")]
public void quit()
{
	bRunning = false;
}
Next, we want another class to hold the structure of functions exposed to Lua, just to keep track of everything and have some fancy help built in:

public class LuaFuncDescriptor
{
	private String FunctionName;
	private String FunctionDoc;
	private ArrayList FunctionParameters;
	private ArrayList FunctionParamDocs;
	private String FunctionDocString;

	public LuaFuncDescriptor(String strFuncName, String strFuncDoc, ArrayList strParams,
							 ArrayList strParamDocs)
	{
		FunctionName = strFuncName;
		FunctionDoc = strFuncDoc;
		FunctionParameters = strParams;
		FunctionParamDocs = strParamDocs;

		String strFuncHeader = strFuncName + "(%params%) - " + strFuncDoc;
		String strFuncBody = "\n\n";
		String strFuncParams = "";

		Boolean bFirst = true;
			
		for (int i = 0; i < strParams.Count; i++)
		{
			if (!bFirst)
				strFuncParams += ", ";

			strFuncParams += strParams[i];
			strFuncBody += "\t" + strParams[i] + "\t\t" + strParamDocs[i] + "\n";

			bFirst = false;
		}

		strFuncBody = strFuncBody.Substring(0, strFuncBody.Length - 1);
		if (bFirst)
			strFuncBody = strFuncBody.Substring(0, strFuncBody.Length - 1);

		FunctionDocString = strFuncHeader.Replace("%params%", strFuncParams) + strFuncBody;
	}

	public String getFuncName()
	{
		return FunctionName;
	}

	public String getFuncDoc()
	{
		return FunctionDoc;
	}

	public ArrayList getFuncParams()
	{
		return FunctionParameters;
	}

	public ArrayList getFuncParamDocs()
	{
		return FunctionParamDocs;
	}

	public String getFuncHeader()
	{
		if (FunctionDocString.IndexOf("\n") == -1)
			return FunctionDocString;

		return FunctionDocString.Substring(0, FunctionDocString.IndexOf("\n"));
	}

	public String getFuncFullDoc()
	{
		return FunctionDocString;
	}
}
We want the function to be called "quit" in Lua, and its helpful doc string just tells the user what it does. We also have a function descriptor to keep track of all added functions. So far sogood, now how we do make Lua aware of those attributes to pick everything up? LuaInterface has nothing like that (yet), but we can make it fairly easily with a function. Let's call it "registerLuaFunctions":

public static void registerLuaFunctions(Object pTarget)
{
	// Sanity checks
	if (pLuaVM == null || pLuaFuncs == null)
		return;

	// Get the target type
	Type pTrgType = pTarget.GetType();

	// ... and simply iterate through all it's methods
	foreach (MethodInfo mInfo in pTrgType.GetMethods())
	{
		// ... then through all this method's attributes
		foreach (Attribute attr in Attribute.GetCustomAttributes(mInfo))
		{
			// and if they happen to be one of our AttrLuaFunc attributes
			if (attr.GetType() == typeof(AttrLuaFunc))
			{
				AttrLuaFunc pAttr = (AttrLuaFunc) attr;
				Hashtable pParams = new Hashtable();

				// Get the desired function name and doc string, along with parameter info
				String strFName = pAttr.getFuncName();
				String strFDoc = pAttr.getFuncDoc();
				String[] pPrmDocs = pAttr.getFuncParams();

				// Now get the expected parameters from the MethodInfo object
				ParameterInfo[] pPrmInfo = mInfo.GetParameters();

				// If they don't match, someone forgot to add some documentation to the
				// attribute, complain and go to the next method
				if (pPrmDocs != null && (pPrmInfo.Length != pPrmDocs.Length))
				{
					Console.WriteLine("Function " + mInfo.Name + " (exported as " +
									  strFName + ") argument number mismatch. Declared " +
									  pPrmDocs.Length + " but requires " +
									  pPrmInfo.Length + ".");
					break;
				}

				// Build a parameter <-> parameter doc hashtable
				for (int i = 0; i < pPrmInfo.Length; i++)
				{
					pParams.Add(pPrmInfo[i].Name, pPrmDocs[i]);
				}

				// Get a new function descriptor from this information
				LuaFuncDescriptor pDesc = new LuaFuncDescriptor(strFName, strFDoc, pParams);

				// Add it to the global hashtable
				pLuaFuncs.Add(strFName, pDesc);

				// And tell the VM to register it.
				pLuaVM.RegisterFunction(strFName, pTarget, mInfo);
			}
		}
	}
}
Now we modify the Program class and constructor to look like this:

public static Hashtable pLuaFuncs = null;

public Program()
{
	pLuaVM = new Lua();
	pLuaFuncs = new Hashtable();
	registerLuaFunctions(this);
}
That's it. Now, as soon as you instantiate the Program class, it will register all it's functions automatically. Since the LuaVM, function hashtable and registerLuaFuncs method are all static, youcan call them from wherever and add new functions to it, keeping a single centralized VM and function list.

We've being adding a lot of "Doc" stuff we have no use for yet. It's time to fix that. The following two functions make all it happen for us:

[AttrLuaFunc("help", "List available commands.")]
public void help()
{
	Console.WriteLine("Available commands: ");
	Console.WriteLine();

	IDictionaryEnumerator Funcs = pLuaFuncs.GetEnumerator();
	while (Funcs.MoveNext())
	{
		Console.WriteLine(((LuaFuncDescriptor)Funcs.Value).getFuncHeader());
	}
}

[AttrLuaFunc("helpcmd", "Show help for a given command", "Command to get help of.")]
public void help(String strCmd)
{
	if (!pLuaFuncs.ContainsKey(strCmd))
	{
		Console.WriteLine("No such function or package: " + strCmd);
		return;
	}

	LuaFuncDescriptor pDesc = (LuaFuncDescriptor)pLuaFuncs[strCmd];
	Console.WriteLine(pDesc.getFuncFullDoc());
}
It's as simple as that, just write those two functions and they get inserted without you having to move a finger. They are straightforward. The first one lists all available commands (exposed from your application) and the second one gives detailed help on any command. Compile, build and try:

help()
helpcmd("help")
helpcmd("helpcmd")
Impressive, huh?

There is one significant improvement that can be made here and it's having functions grouped in packages for each object registering it's functions. Think of it as an exercise left to the reader.In any case, it's included in the full project attached to this article.



What is LUA? LUA is a scripting language, its power lies in the fact that it can be embedded in your C++ programs. Scripts give you the possibility to change the behaviour of your C++ programs without any need to recompile your program. A real world example would be game programming: you only have to change the script(s) and you can create a whole new game using the same engine. LUA is fully customizable, you can create your own script functions and expose them to a 3th party. Or you can create scripts, encrypt them and then decrypt them at run-time so that only a number of limited people can script for your program. Allowing the end-user to modify your program, gives your program a point ahead to the competition. Using LUA in your program does require some thinking ahead... You have to choose what kind of functions you allow in the scripts. For example: a function savegame would be logic and usefull and so could be a function deletesavegame but making a function such as deletefile public can be dangerous. The LUA version discussed is 5.0.2. Don't forget that this document is only a quick introduction and that it is not a complete tutorial about LUA. How to embed LAU into C++? Another way to formulate this question : "How can I use LUA in my Visual C++ Project?". The answer is actually pretty easy, you download it from lua.org and you follow the instructions below. Know that there are several ways to add LUA to your project and I only explain one of them. NOTE: I'm assuming that you know the basics about how to use your compiler, the instructions outlined below are ment for Microsoft Visual C++ 6. Installation : dowload LUA here (links to official website) Extract the files to a folder, for example "c:\Program Files\LUA SDK" Configuration : In your Microsoft Visual C++ IDE Go to Tools->Options. Select Directories and add the LUA include-directory. Add LUA to your project (non-MFC) : Create a new folder in your project's workspace and call it LUA. Add the files. Select and insert all the files in the LUA src-directory (from lapi.c to lzio.h). Add LUA to your project (MFC) : Create a new folder in your project's workspace and call it LUA. Add the files. Select and insert all the files in the LUA src-directory (from lapi.c to lzio.h). MFC uses precompiled headers, LUA doesn't, so let's disable them. Select the LUA folder in your workspace and press the right mouse button, then select Settings from the menu. Select "All Configurations". Then open the LUA folder and select all the .c-files (make sure your selection doesn't include a folder or a .h file!). On the right side a C++-tab will appear. In that C++-tab select the "Precompiled Headers"-catagory. And select "Not using precompiled headers". Press the OK-button to finish. About ANSI C and C++ LUA is pure ANSI C code, this means that if you build the code with a C++ compiler it will complain with "error LNK2001: unresolved external symbol" messages. Two easy ways exist to resolve this problem without modifying the original source files : Tell the compiler that the function definitions are C style by enclosing the include directive with the extern keyword : extern "C" { #include <lua.h> } You can also define LUA_API before including lua.h : #define LUA_API extern "C" #include <lua.h> I recommend that you use the first method. The LUA State In order to use LUA you have to initialize it and when you're done with it you have to deinitialize LUA. This is done by respectivily opening and closing an LUA state. // Open the LUA state lua_State *L = lua_open(); // Use LUA... ; // Close the LUA state lua_close(L); You can have multiple LUA states in your program which are all indepedent of each other. The LUA Stack LUA is stack-based. The communication between the script and the C/C++ application happens between a stack maintained by LUA. Note that each LUA state has its own stack. A clean programmer will ensure that the stack is zero at the end of his program. You can verify this by calling the lua_gettop function, the result must be zero, it's a good candidate for the _ASSERT macro (defined in crtdbg.h). // Open the LUA state lua_State *L = lua_open(); // Use LUA... ; // Verify the stack // (don't forget to include crtdbg.h) _ASSERT(lua_gettop(L) == 0); // Close the LUA state lua_close(L); LUA defines lua_pushXXX (where XXX can be "string", "number", "boolean", ...) but it doesn't define the lua_popXXX versions. Here's an example how to define them by yourself : inline LUA_API lua_Number lua_popnumber(lua_State *L) { register lua_Number tmp = lua_tonumber(L, lua_gettop(L)); lua_pop(L, 1); return tmp; } inline LUA_API const char *lua_popstring(lua_State *L) { register const char *tmp = lua_tostring(L, lua_gettop(L)); lua_pop(L, 1); return tmp; } When popping the values they are converted automaticly when possible : // Push -1 as a number lua_pushnumber(L, -1); // And pop it again as a string (s --> "-1") const char *s = lua_popstring(L); If the conversion is impossible then NULL/0 will be returned. For example we can't convert a boolean to a string : // Push 1 (true) lua_pushboolean(L, 1); // And pop it again as a string (s --> NULL) const char *s = lua_popstring(L); There are many other stack manipulation functions and there are also functions to verify the value type on the stack. I suggest that you check out the LUA manual that comes with the distribution. Executing an LUA script Executing an LUA script isn't that straight-forward at first but at the end it turns out the be very simple. I'm explaining you the full implementation of how to execute a script by creating your own "reader". LUA comes with its own library that contains ready-to-use functions but I prefer you to explain the complete version so you can understand better how LUA works. In order to execute LUA scripts you have to load them first and call them afterwarts, this is done by respectivily calling the lua_load and the lua_call or lua_pcall function. The lua_load function takes four parameters : LUA_API int lua_load (lua_State *L, lua_Chunkreader reader, void *data, const char *chunkname); The first one is the pointer to the LUA state, the second one is a pointer to a user-defined reader function, the third pointer is a user-defined value that the reader function will receive, and the fourth one is a name we decide ourselves for debugging purposes. As you can see from this call, there is no argument accepting a script. We have to define this ourselves using the data argument. Let's take a look at this structure (taken from the LUA library), we will define : typedef struct luaMemFile { const char *text; size_t size; } luaMemFile; The text variable will be our script line(s) and the size variable will tell us if we have finished reading or not (see later). A value of zero will mean : we are no longer reading. We now define a simple callback function : const char *readMemFile(lua_State *, void *ud, size_t *size) { // Convert the ud pointer (UserData) to a pointer of our structure luaMemFile *luaMF = (luaMemFile *) ud; // Are we done? if(luaMF->size == 0) return NULL; // Read everything at once // And set size to zero to tell the next call we're done *size = luaMF->size; luaMF->size = 0; // Return a pointer to the readed text return luaMF->text; } Once the script has been loaded, a simple call to lua_pcall will suffice. I prefer lua_pcall above lua_call because the later one will terminate the program if there was an error. The lua_call takes three parameters and the lua_pcall function takes four parameters : LUA_API void lua_call (lua_State *L, int nargs, int nresults); LUA_API int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc); The first parameter is the pointer to the LUA state, the second parameter is the number of arguments on the stack that the command takes (and when executing a loaded script this can remain 0). The third parameter is the number of result parameters the command will return on the stack. If nresult equals LUA_MULTRET then the number of parameters that will be pushed on the stack is controlled by the called function! Yes indeed, an LUA function can return more than one parameter, for example : return "i", "return", 5, "strings", "and", 2, "numbers" The fourth parameter (errfunc) is only valid for the lua_pcall function and is used for controlling the error-handling. Please read the LUA manual for more information about this parameter. A note about the nresults argument: if we take the example return statement above, then we see that 7 values would be returned. If we execute this script with nresult==LUA_MULTRET then 7 values would be returned, if we would call the function with nresult==3 then only the 3 first values would be returned ("i", "return" and 5). This is the test script we will execute : const char *testscript = { "function double(n)\n" " return n * 2\n" "end\n" "\n" "return double(1)\n" }; function double(n) return n * 2 end return double(1) A quick but nasty way of executing would be this : // luaMemFile luaMF; // Load the command and try to execute it... luaMF.text = testscript; luaMF.size = strlen(luaMF.text); lua_load(L, readMemFile, &luaMF, "test script"); lua_pcall(L, 0, 0, 0); You may ask why is this a bad method? Well, there is no error checking, if the script contains error or an execution error occur than the stack will be messed-up, LUA functions push their error values on the stack so you need to check the result of both lua_load and lua_pcall and act accordingly! This is the implementation that I prefer : // luaMemFile luaMF; // Load the command and try to execute it... luaMF.text = testscript; luaMF.size = strlen(luaMF.text); if(lua_load(L, readMemFile, &luaMF, "test script") == 0) { // Execute the loaded command... // The function takes 0 parameters and will return 1 result if(lua_pcall(L, 0, 1, 0) == 0) { // There was no error // Let's get the result from the stack lua_Number result = lua_tonumber(L, lua_gettop(L)); } // Clean-up the stack lua_pop(L, 1); } else { // There was a lua_load error... // Pop the error value from the stack lua_pop(L, 1); } Calling C functions from LUA In order to make this topic more accessible, a whole new page has been assigned to it, please follow this link : Calling C functions from LUA or How to expose C function to LUA scripts. 来源: http://www.codegurus.be/codegurus/Programming/luaintroduction_en.htm
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值