wiremod / wire-cpu

Legacy CPU/GPU/SPU as a separate addon.
Apache License 2.0
8 stars 6 forks source link

CPULib Extension framework #45

Closed DerelictDrone closed 9 months ago

DerelictDrone commented 10 months ago

Extends CPULib to allow for addons to add new instructions to a ZVM platform on a per-entity basis, determined by the player's load order at entity creation.

The UI to load and unload extensions is available in the tool menu, clicking on an extension will switch it between the two categories image

It will only show the available extensions for said tool, you won't see GPU extensions when looking at the CPU tool for example.

The HLZASM compiler & editor are aware of your currently available extended instructions, and can only highlight or compile them while enabled

(extension is enabled) image

(extension is disabled) image

Compilation/upload is also affected by the player's load order, so keep in mind if the load order differs between player & ent your code may not work correctly.

See cpu_example_extension.lua as an example for the structure required to make your own extension.

Vurv78 commented 9 months ago

Think the api should be made as simple as possible to avoid breaking changes and extension authors making mistakes

local myCPUExtension = {
    Platform = "CPU",
    Instructions = {{
        Name = "CPU_TEST1",
        Operands = 1,
        Version = 0.42,
        Flags = {"W1"}, -- writes first operand
        Op1Name = "X",
        Op2Name = "",
        Description = "Sets Register X to constant 42",
        ["OpFunc"] = function(self)
            -- The end value of the code in Dyn_EmitOperand will be assigned
            -- to the first/left hand register used in this instruction
            self:Dyn_EmitOperand("42")
        end
    },
    {
        Name = "CPU_TEST2",
        Operands = 1,
        Version = 0.42,
        Flags = {"W1"}, -- writes first operand
        Op1Name = "X",
        Op2Name = "",
        Description = "Divides Register X by constant 24",
        ["OpFunc"] = function(self)
            -- $1 and $2 refer to the first, and second operands of the instruction respectively
            self:Dyn_EmitOperand("$1/24")
        end
    }}
}

To

local MyExtension = CPULib.RegisterCPUExtension("name" --[[, version? ]])

--- Divides Register X by constant 24 (Yes you could extract this with debug.getinfo and reading the line above the function.)
MyExtension:RegisterInstruction("XYZ", { "X" }, function(self)
    self:DynEmitOperand("$1/24") -- I'd like to swap this out too in favor of a safer api that you can be assured you won't want to remove later on. But it's up to you
end, { "W1" }) -- The "write" flag probably should be included in the actual argument. But I don't know enough about its purpose to give an ideal answer

Biggest concern here is extension authors making a bunch of addons with easy code injection, which is why I included the comment about another api.

Already Starfall has kind of had that issue before, not as much because it already has everything builtin. ZCPU doesn't have anything builtin, so potentially a lot of addons could be made for it which makes this very concerning.

Can't really think of a safer transpiled api. It would be much safer if it just transpiled extension operations to call the provided function, like E2 / SF. The performance hit would be worth it over opening up addon authors to make a ton of RCEs. Could provide this current behavior as a separate, raw api for competent devs.

DerelictDrone commented 9 months ago
local MyExtension = CPULib.RegisterCPUExtension("name" --[[, version? ]])

--- Divides Register X by constant 24 (Yes you could extract this with debug.getinfo and reading the line above the function.)
MyExtension:RegisterInstruction("XYZ", { "X" }, function(self)
    self:DynEmitOperand("$1/24") -- I'd like to swap this out too in favor of a safer api that you can be assured you won't want to remove later on. But it's up to you
end, { "W1" }) -- The "write" flag probably should be included in the actual argument. But I don't know enough about its purpose to give an ideal answer

So, for helping understand the above. The only necessary things for the actual creation of an instruction is the Name(for the IDE), Operand Count, Flags, and the OpFunc(which generates the transpiled code)

For flags, they're primarily used during the HLZASM compilation (to the bytecode), the only one that affects the precompile at runtime is the Privileged flag (R0), which wraps the precompile in a check for the current page's runlevel and interrupts if not present.

The rest of this(Opname1 and 2, version, and description) is used for the generation of the E2helper docs (like here) image

But since I don't necessarily have an easy way to modify them from this repo in a way that would allow me to add and remove them as the extensions are enabled and disabled (I would need to PR support for this to wire-main) their inclusion is only for future support

It would be much safer if it just transpiled extension operations to call the provided function, like E2 / SF. The performance hit would be worth it over opening up addon authors to make a ton of RCEs. Could provide this current behavior as a separate, raw api for competent devs.

I could certainly make a function that creates a dummy instruction which just calls a user provided function with args and have CPULib load the VM with a copy of the funcs, if that sounds good with you

DerelictDrone commented 9 months ago

@Vurv78 Check the API used here and tell me what you think of it, vs the previous one

It should be noted however, should #48 come to pass, these lua funcs might have global scope while the instructions themselves will still remain in the VM's env scope, but since the lua func itself isn't generated from any compilestring, it should be fine?

Vurv78 commented 9 months ago

@Vurv78 Check the API used here and tell me what you think of it, vs the previous one

It should be noted however, should #48 come to pass, these lua funcs might have global scope while the instructions themselves will still remain in the VM's env scope, but since the lua func itself isn't generated from any compilestring, it should be fine?

Yeah that should be fine. It's best this way too, I was going to say that if setfenv were to be used it'd make a transpilation api very confusing if they wanted to access globals