Exporting a class between two resources

Hello,

I’ve been recently setting up some lua-based classes, until I found out exporting or passing an instance between resources wouldn’t work. A similar issue has already been opened in here.

It seems that the class is indeed passed as a table, but none of the metatable __index instance methods are binded. A simple example is:

Marker = {}
Marker.__index = Marker

function Marker:new(type)
    local marker = {}
    setmetatable(marker, Marker)
    marker.type = type or 0
    return marker
end

function Marker:print()
    print(self.type)
end

Passing Marker:new() to another resource will have all its methods as undefined, print isn’t defined.

I’m not exactly sure if this fits as a feature request, but I couldn’t find any discussion about it previously. For that reason, I decided to open a request here to find out if there’s any particular reason for this behaviour or if there are any intentions to change it.

Even though I can work around it with plain tables, “classes” (with the self syntactic sugar and its metatable) can become really useful to share data or complex logic between multiple resources.

1 Like

I have very little experience with Lua, so there’s a chance that what I’m about to say is BS.
Did you try to put the class(es) in a certain resource, then include it from other resources by including it in the manifest? i.e.

client_scripts {
    '@libraryResourceName/cMarker.lua',
    'client/main.lua',
    'client/other.lua'
}

Yes, it indeed works. But that kinda defeats the point of the exports, since it seems to import the class file instead.

Let’s say I want the @libraryResourceName to dynamically create a Marker, store it and return to the caller. Instead of having to include each class in the script that intends to use it, create it, and pass it to @libraryResourceName - I would simply pass it as an export.

Sorry for replying so late…
I’m not much of a LUA scripter but hopefully this is still applicable…

If it helps, as I learned myself, using exports is more about inter-resource functionality…

Let your class handle all of it’s own data internally, and then use exports to expose the public functionality of that module…

It took me a LONG time to migrate from this mindset - believe me, I fought mods over it. (Sorry mods!)
You can’t export full classes. You can only expose (data-level) access to those classes via exports.
Not pure functions.

Why? Because FiveM is a multi-language platform. You can’t export LUA ‘code’ into it. I can’t export JS ‘code’ into it.

But you CAN give it data, and let it give you data, and let it control and manage your game mode.

Hopefully I am making sense.
Your classes need to be a closed box that nothing else can see.
And then you use exports to use the functionality within that box.

Good luck!

(Sorry mods. Hopefully this support tenure is making up for it slightly <3)

1 Like

To add to the above (and the linked post), there was a long-term goal to expose some sort of ‘meta-refs’ as in the ability to wrap an object into some sort of uniform metatable/proxy/dynamic object for deserialization elsewhere, but this’d have unknown performance characteristics that might have turned out to be remarkably problematic, so it never left the concept phase.

If you want to make such a thing yourself, you probably can, though - just keep in mind that you’d have to handle deserializing the object into a local language projection yourself (unless your favorite language exposes msgpack extension registration), it’d not have involved anything too different from __index, __newindex etc. and its equivalents in other languages exposed into a ‘ref function’ in the serialization ABI.

2 Likes

@TheWaste @nta

I strongly agree. I’m very close to giving up on the idea of exporting a “full class”. From a serialization point of view, it makes complete sense that the exports or events are supposed to be plain data.

What I wrote above is indeed possible if instead of using the metatable (setmetatable(marker, Marker), I actually set the attributes on the table itself (kind of a class hard-clone, without any prototypes). It was one of my first alternatives.

My only regret is that the idea of abusing the execution context and invoke functions from other resources could be very useful to avoid having to include each class definition on the respective resource.

Consider I want to export the above Marker, exposing data only:

  • Add ‘marker.lua’ to the second resource
  • Instantiate a new marker with the received data: Marker:new(type)

It becomes impractical, even worse if you scale it. Unless you have any alternative?
In any case, I may stick around with some solution that wraps the object in some sort of non-meta based table. Thanks for your suggestions.

Disclaimer; I’m not a LUA scripter, I’m not sure how metatables work…

Frankly - personally - I would abandon the idea of exporting a ‘full class’.
You can only expose the controls which operate it, via exports

Other developers would probably disagree, but using exports to eventually “extend” functionality from a different resource is frankly not possible in the way you are intending.
Well, actually it is, but it is complicated. Frankly, not worth the hassle.

Actually I have found emit/TriggerEvent works just as well as export.
It’s basically the same thing, but also adds server-to-client communication.

If you want to ‘package away’ your Marker creation…
if that is the heavy lifting that your code is doing…
then go ahead and make Marker.lua use exports.
All of your resources will be able to call upon it via export.

But personally… I just copy-paste main re-usable functions between projects.
Marker spawning, text rendering… I just copy paste it. It’s just easier…

I use the exports for heavy lifting operations that my gamemodes “shouldn’t see”…
For example, I have a gamemode which submits scores to a high-score database.

My gamemode doesn’t need to concern itself with scorekeeping…
It just sends a bunch of score data off to the export, and the export does it’s thing.
When it’s finished, it replies via TriggerEvent with an updated/sorted score table.

To clarify also, Marker would run continuously in the background. It does not start and stop just for an export. Once your server loads and the resource is active, it will listen for exports… it doesn’t need to re-execute/re-intialise each time. It resembles a Singleton pattern in that way… a unified topology.

You can cheat with export in lua by creating classes without using metatables.
I don’t know about performances of this method but it’s quite simple.

Resource : Test1

function GenerateAFakeClass(value1, value2)
    self = {}

    self.value1 = value1
    self.value2 = value2

    self.GetValue1 = function()
        return value1
    end

    self.SetValue1 = function(newValue)
        value1 = newValue
    end

    self.GetValue2 = function()
        return value2
    end

    self.SetValue2 = function(newValue)
        value2 = newValue
    end

    return self
end

local FakeInstance = GenerateAFakeClass(1, 7)

exports("TestExport", function()
    return FakeInstance
end)

Resource : Test2

local FakeInstance = exports["Test1"]:TestExport()

print(FakeInstance.GetValue1())
print(FakeInstance.GetValue2())
FakeInstance.SetValue1(2)
FakeInstance.SetValue2(9)
print(FakeInstance.GetValue1())
print(FakeInstance.GetValue2())

Console :

> Ensure Test1
[    c-scripting-core] Creating script environments for Test1
[           resources] Started resource Test1
> Ensure Test2
[    c-scripting-core] Creating script environments for Test2
[        script:Test2] 1
[        script:Test2] 7
[        script:Test2] 2
[        script:Test2] 9
[           resources] Started resource Test2

The main problem is that :

local FakeInstance = exports["Test1"]:TestExport()

print(FakeInstance.GetValue1())
print(FakeInstance.value1)
FakeInstance.SetValue1(2)
print(FakeInstance.GetValue1())
print(FakeInstance.value1)
[        script:Test2] 1
[        script:Test2] 1
[        script:Test2] 2
[        script:Test2] 1

Values are not passed by ref but by value.