Added http.request to Lua, renamed Lua.PushValue to Lua.PushObject, added lua.lua REPL

This commit is contained in:
Alessandro Proto 2023-01-08 18:41:17 +01:00
parent c4d5ebd500
commit 9d978fd9ab
12 changed files with 282 additions and 37 deletions

3
.gitignore vendored
View file

@ -27,7 +27,8 @@ x86/
[Aa][Rr][Mm]/ [Aa][Rr][Mm]/
[Aa][Rr][Mm]64/ [Aa][Rr][Mm]64/
bld/ bld/
[Bb]in/ /[Bb]in/
/[Cc]apy64/bin/
[Oo]bj/ [Oo]bj/
[Oo]ut/ [Oo]ut/
[Ll]og/ [Ll]og/

View file

@ -11,6 +11,6 @@ namespace Capy64.API;
public interface IPlugin public interface IPlugin
{ {
void ConfigureServices(IServiceCollection services) { } void ConfigureServices(IServiceCollection services) { }
void LuaInit(Lua state) { } void LuaInit(Lua L) { }
} }

View file

@ -0,0 +1,78 @@
local term = require("term")
local io = require("io")
local colors = require("colors")
local colours = colors
local tArgs = { ... }
if #tArgs > 0 then
print("This is an interactive Lua prompt.")
print("To run a lua program, just type its name.")
return
end
--local pretty = require "cc.pretty"
local bRunning = true
local tCommandHistory = {}
local tEnv = {
["exit"] = setmetatable({}, {
__tostring = function() return "Call exit() to exit." end,
__call = function() bRunning = false end,
}),
["_echo"] = function(...)
return ...
end,
}
setmetatable(tEnv, { __index = _ENV })
for k, v in pairs(package.loaded) do
tEnv[k] = v
end
term.setForeground(colours.yellow)
print("Interactive Lua prompt.")
print("Call exit() to exit.")
term.setForeground(colours.white)
while bRunning do
term.setForeground( colours.yellow )
write("lua> ")
term.setForeground( colours.white )
local s = io.read(nil, tCommandHistory)
if s:match("%S") and tCommandHistory[#tCommandHistory] ~= s then
table.insert(tCommandHistory, s)
end
local nForcePrint = 0
local func, e = load(s, "=lua", "t", tEnv)
local func2 = load("return _echo(" .. s .. ");", "=lua", "t", tEnv)
if not func then
if func2 then
func = func2
e = nil
nForcePrint = 1
end
else
if func2 then
func = func2
end
end
if func then
local tResults = table.pack(pcall(func))
if tResults[1] then
local n = 1
while n < tResults.n or n <= nForcePrint do
local value = tResults[n + 1]
print(tostring(value))
n = n + 1
end
else
print(tResults[2])
end
else
print(e)
end
end

View file

@ -10,4 +10,4 @@ term.setPos(1,1)
print(os.version()) print(os.version())
dofile("/bin/shell.lua") dofile("/bin/lua.lua")

View file

@ -144,7 +144,7 @@ public class Bios : IPlugin
public static void InstallOS(bool force = false) public static void InstallOS(bool force = false)
{ {
var installedFilePath = Path.Combine(FileSystem.BasePath, ".installed"); var installedFilePath = Path.Combine(Capy64.AppDataPath, ".installed");
if (!File.Exists(installedFilePath) || force) if (!File.Exists(installedFilePath) || force)
{ {
FileSystem.CopyDirectory("Assets/Lua", FileSystem.DataPath, true, true); FileSystem.CopyDirectory("Assets/Lua", FileSystem.DataPath, true, true);

View file

@ -15,12 +15,18 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using static Capy64.Utils; using static Capy64.Utils;
using System.IO;
namespace Capy64; namespace Capy64;
public class Capy64 : Game, IGame public class Capy64 : Game, IGame
{ {
public const string Version = "Capy64 a0.0.1"; public const string Version = "a0.0.1";
public static string AppDataPath = Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData,
Environment.SpecialFolderOption.Create),
"Capy64");
public Game Game => this; public Game Game => this;
public IList<IPlugin> NativePlugins { get; private set; } public IList<IPlugin> NativePlugins { get; private set; }
public IList<IPlugin> Plugins { get; private set; } public IList<IPlugin> Plugins { get; private set; }
@ -102,7 +108,7 @@ public class Capy64 : Game, IGame
protected override void Initialize() protected override void Initialize()
{ {
Window.Title = Version; Window.Title = "Capy64 " + Version;
UpdateSize(); UpdateSize();

View file

@ -10,6 +10,7 @@ namespace Capy64;
public interface IGame public interface IGame
{ {
Game Game { get; } Game Game { get; }
IList<IPlugin> NativePlugins { get; } IList<IPlugin> NativePlugins { get; }
IList<IPlugin> Plugins { get; } IList<IPlugin> Plugins { get; }

View file

@ -11,31 +11,31 @@ using System.Threading.Tasks;
namespace Capy64.LuaRuntime.Extensions; namespace Capy64.LuaRuntime.Extensions;
public static class Utils public static class Utils
{ {
public static void PushArray(this Lua state, object obj) public static void PushArray(this Lua L, object obj)
{ {
var iterable = obj as IEnumerable; var iterable = obj as IEnumerable;
state.NewTable(); L.NewTable();
long i = 1; long i = 1;
foreach (var item in iterable) foreach (var item in iterable)
{ {
state.PushValue(item); L.PushObject(item);
state.RawSetInteger(-2, i++); L.RawSetInteger(-2, i++);
} }
state.SetTop(-1); L.SetTop(-1);
} }
#nullable enable #nullable enable
public static int PushValue(this Lua state, object? obj) public static int PushObject(this Lua L, object? obj)
{ {
var type = obj?.GetType(); var type = obj?.GetType();
switch (obj) switch (obj)
{ {
case string str: case string str:
state.PushString(str); L.PushString(str);
break; break;
case char: case char:
state.PushString(obj.ToString()); L.PushString(obj.ToString());
break; break;
case byte: case byte:
@ -45,37 +45,37 @@ public static class Utils
case int: case int:
case uint: case uint:
case double: case double:
state.PushNumber(Convert.ToDouble(obj)); L.PushNumber(Convert.ToDouble(obj));
break; break;
case long l: case long l:
state.PushInteger(l); L.PushInteger(l);
break; break;
case bool b: case bool b:
state.PushBoolean(b); L.PushBoolean(b);
break; break;
case null: case null:
state.PushNil(); L.PushNil();
break; break;
case byte[] b: case byte[] b:
state.PushBuffer(b); L.PushBuffer(b);
break; break;
case LuaFunction func: case LuaFunction func:
state.PushCFunction(func); L.PushCFunction(func);
break; break;
case IntPtr ptr: case IntPtr ptr:
state.PushLightUserData(ptr); L.PushLightUserData(ptr);
break; break;
default: default:
if (type is not null && type.IsArray) if (type is not null && type.IsArray)
{ {
state.PushArray(obj); L.PushArray(obj);
} }
else else
{ {
@ -87,15 +87,16 @@ public static class Utils
return 1; return 1;
} }
public static void PushManagedObject<T>(this Lua state, T obj) [Obsolete("This method does not work as intended and requires more research")]
public static void PushManagedObject<T>(this Lua L, T obj)
{ {
var type = obj.GetType(); var type = obj.GetType();
var members = type.GetMembers().Where(m => m.MemberType == MemberTypes.Method); var members = type.GetMembers().Where(m => m.MemberType == MemberTypes.Method);
state.CreateTable(0, members.Count()); L.CreateTable(0, members.Count());
foreach (var m in members) foreach (var m in members)
{ {
state.PushCFunction(L => (int)type.InvokeMember(m.Name, BindingFlags.InvokeMethod, null, obj, new object[] { L })); L.PushCFunction(L => (int)type.InvokeMember(m.Name, BindingFlags.InvokeMethod, null, obj, new object[] { L }));
state.SetField(-2, m.Name); L.SetField(-2, m.Name);
} }
} }
} }

View file

@ -12,8 +12,7 @@ namespace Capy64.LuaRuntime.Libraries;
public class FileSystem : IPlugin public class FileSystem : IPlugin
{ {
public static string BasePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "Capy64"); public static string DataPath = Path.Combine(Capy64.AppDataPath, "data");
public static string DataPath = Path.Combine(BasePath, "data");
public FileSystem() public FileSystem()
{ {
@ -451,7 +450,7 @@ public class FileSystem : IPlugin
foreach (var attribute in attributes) foreach (var attribute in attributes)
{ {
L.PushString(attribute.Key); L.PushString(attribute.Key);
L.PushValue(attribute.Value); Extensions.Utils.PushObject(L, attribute.Value);
L.SetTable(-3); L.SetTable(-3);
} }

View file

@ -2,21 +2,180 @@
using KeraLua; using KeraLua;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.LuaRuntime.Libraries; namespace Capy64.LuaRuntime.Libraries;
#nullable enable
public class HTTP : IPlugin public class HTTP : IPlugin
{ {
private static IGame _game; private static IGame _game;
private static HttpClient _client;
private static long RequestId;
private readonly LuaRegister[] HttpLib = new LuaRegister[]
{
new()
{
name = "request",
function = L_Request,
},
new()
{
name = "checkURL",
function = L_CheckUrl,
},
new(),
};
public HTTP(IGame game) public HTTP(IGame game)
{ {
_game = game; _game = game;
RequestId = 0;
_client = new();
_client.DefaultRequestHeaders.Add("User-Agent", $"Capy64/{Capy64.Version}");
} }
public void LuaInit(Lua state) public void LuaInit(Lua L)
{ {
L.RequireF("http", Open, false);
}
private int Open(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.NewLib(HttpLib);
return 1;
}
public static bool TryGetUri(string url, out Uri? uri)
{
return (Uri.TryCreate(url, UriKind.Absolute, out uri)
&& uri?.Scheme == Uri.UriSchemeHttp) || uri?.Scheme == Uri.UriSchemeHttps;
}
private static int L_Request(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var request = new HttpRequestMessage();
var url = L.CheckString(1);
if (!TryGetUri(url, out Uri? uri) || uri is null)
{
L.ArgumentError(1, "invalid request url");
return 0;
}
request.RequestUri = uri;
if (L.IsTable(3)) // headers
{
L.PushCopy(3);
L.PushNil();
while (L.Next(-2))
{
L.PushCopy(-2);
var k = L.CheckString(-1);
if (L.IsStringOrNumber(-2))
{
var v = L.ToString(-2);
request.Headers.Add(k, v);
}
else if (L.IsNil(-2))
{
request.Headers.Remove(k);
}
else
{
L.ArgumentError(3, "string, number or nil expected, got " + L.TypeName(L.Type(-2)) + " in field " + k);
}
L.Pop(2);
}
L.Pop(1);
}
var options = new Dictionary<string, object>
{
["binary"] = false,
};
if (L.IsTable(4)) // other options?
{
L.PushCopy(4);
L.PushNil();
while (L.Next(-2))
{
L.PushCopy(-2);
var k = L.CheckString(-1);
switch (k)
{
case "method":
options["method"] = L.CheckString(-2);
break;
case "binary":
options["binary"] = L.IsBoolean(-2) ? L.ToBoolean(-2) : false;
break;
}
L.Pop(2);
}
L.Pop(1);
}
if (!L.IsNoneOrNil(2))
{
if ((bool)options["binary"])
{
request.Content = new ByteArrayContent(L.CheckBuffer(2));
}
else
{
request.Content = new StringContent(L.CheckString(2));
}
}
request.Method = options.TryGetValue("method", out var value)
? new HttpMethod((string)value)
: request.Content is not null ? HttpMethod.Post : HttpMethod.Get;
var requestId = RequestId++;
var reqTask = _client.SendAsync(request);
reqTask.ContinueWith(async (task) =>
{
var response = await task;
object content;
if ((bool)options["binary"])
content = await response.Content.ReadAsByteArrayAsync();
else
content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
_game.LuaRuntime.PushEvent("http_response", requestId, content, (int)response.StatusCode);
else
_game.LuaRuntime.PushEvent("http_failure", requestId, content, (int)response.StatusCode, response.ReasonPhrase);
});
L.PushInteger(requestId);
return 1;
}
private static int L_CheckUrl(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var url = L.CheckString(1);
var isValid = TryGetUri(url, out _);
L.PushBoolean(isValid);
return 1;
} }
} }

View file

@ -34,7 +34,7 @@ public class OS : IPlugin
{ {
var L = Lua.FromIntPtr(state); var L = Lua.FromIntPtr(state);
L.PushString(Capy64.Version); L.PushString("Capy64 " + Capy64.Version);
return 1; return 1;
} }

View file

@ -124,7 +124,7 @@ public class Runtime
{ {
foreach (var par in ev.Parameters) foreach (var par in ev.Parameters)
{ {
Thread.PushValue(par); Thread.PushObject(par);
} }
} }
return (ev.Parameters?.Length ?? 0) + 1; return (ev.Parameters?.Length ?? 0) + 1;