Dynamic Linking in Lua for LInux Version

I am developing a math intensive 3D extension for Aseprite, and I’m testing on Linux. I can’t get the extension to correctly open the .so file. I currently have a very simple c++ test code to see if I can get the binary file to load. But I get this always (tried Github source compiled Linux version and Steam Linux Version)

[string “internal”]:7: module ‘binary-test/testmodule’ not found:
no field package.preload[‘binary-test/testmodule’]
no file ‘/home/usuario/.config/aseprite/extensions/binary-test/testmodule.lua’
no file ‘/usr/local/share/lua/5.4/testmodule.lua’
no file ‘/usr/local/share/lua/5.4/testmodule/init.lua’
no file ‘/usr/local/lib/lua/5.4/testmodule.lua’
no file ‘/usr/local/lib/lua/5.4/testmodule/init.lua’
no file ‘./testmodule.lua’
no file ‘./testmodule/init.lua’
no file ‘/usr/local/lib/lua/5.4/binary-test/testmodule.so’
no file ‘/usr/local/lib/lua/5.4/loadall.so’
no file ‘./binary-test/testmodule.so’

I have checked my packager script (zips and renames to .aseprite-extension) and the resulting extension file + the contents of the install folder in .config/aseprite/extensions/binary-test/ and the .so file is always present, though noticeably not in any of the actual locations Aseprite’s LUA sandbox searches in (though the last location in the warning should work).

Is there any way to actually have the program open the binary’s exposed function? Without having the user have to move the file to some arcane location in usr/local/lib. Do I need to re-compile my source-code version with different tags?

Ok, so I spent a whole day on this and the solution was pretty obvious, you just have to use LoadLib, and if you want to use require, make sure you have updated cpath with the relative location.

I have an example repo with a test extension that works on Ubuntu 25.04, Aseprite Ver 1.3.16-beta1-x64

Here’s the (important) code showing how to load using loadlib luaopen_ and optionally require strategy too

– read the extension name, either statically (un-updated defaultName) or from the extension package.json (parse from location+possibleFiles)
local function readExtensionName()
local defaultName = “binary-test”
local possibleFiles = {“package.json”}

– get the execution location and append the possibleFiles values in the Table
local info = debug.getinfo(1, “S”)
if info and info.source then
local sourcePath = info.source
if sourcePath:sub(1, 1) == “@” then
sourcePath = sourcePath:sub(2)
end
local dir = sourcePath:match(“^(.*[/\])”)
if dir then
table.insert(possibleFiles, 1, dir .. “package.json”)
end
end

– iterate the possible .json locations, and search the “name” item in the package file
for _, path in ipairs(possibleFiles) do
— open file location
local f = io.open(path, “r”)
if f then
local contents = f:read(“a")
f:close()
– Lookup “name” field in file in location
local name = contents:match('“name”%s:%s*”([^“]+)”')
if name and #name > 0 then
return name
end
end
end

– return either the static defaultName (set at beggining) or the updated value through the package.json file
return defaultName
end

–Setup values for constructing the extension’s directory in Ubuntu Linux (modify extensionRoot depending on .config location on different Linux Distro)
local extensionName = readExtensionName()
local homeDir = os.getenv(“HOME”) or “~”
local extensionRoot = homeDir .. “/.config/aseprite/extensions/” .. extensionName

–Statically save the module name
local moduleName = “testmodule”
local mod = nil

– construct paths
local candidatePaths = {
extensionRoot .. “/” .. moduleName .. “.so”,
extensionRoot .. “/lib/” .. moduleName .. “.so”,
extensionRoot .. “/bin/” .. moduleName .. “.so”
}

local loaderName = “luaopen_” .. moduleName
for _, path in ipairs(candidatePaths) do
local loader, loadErr = package.loadlib(path, loaderName)
if loader then
local ok, result = pcall(loader)
if ok then
mod = result
package.loaded[moduleName] = mod
print("Loaded testmodule via package.loadlib from " .. path)
break
else
print("Error invoking loader from " .. path .. “:”, result)
end
elseif loadErr then
print("package.loadlib failed on " .. path .. “:”, loadErr)
end
end

– Alternative cpath update path for loading with “require” (doesn’t work for me but might for you)
–append the .so filetypes with to the final constructed location with wildcard glob pattern (“?”)
local newEntries = {
extensionRoot .. “/?.so”,
extensionRoot .. “/?/init.so”
}

– update the lookup path (cpath) for .so files with the new locations+filetypes in newEntries for “require” call (we dont use require, but loadlib anyway, do this if you want to be extensive)
local updatedCpath = package.cpath
for _, entry in ipairs(newEntries) do
if not updatedCpath:find(entry, 1, true) then
updatedCpath = entry .. “;” .. updatedCpath
end
end
package.cpath = updatedCpath