In this setup, all of these components are built in Nim; and a.exe and c.so both depend on b.so.
I'm currently doing this by having both a b.nim and a b_api.nim.
b.nim (used in b.so):
type Thing = object
test_member: int
proc do_thing*(thing: Thing) {.exportc, dynlib, cdecl} =
# Lots of code
b_api.nim (used in a.exe and c.so)
# Redeclare the used types
type Thing = object
test_member: int
# Redeclare the procedure, but import the body from the .so
proc do_thing*(thing: Thing) {.importc, dynlib:"b.so", cdecl}
This works, but obviously involves a lot of manual duplication. Is there an easier way? For example, in C/C++ land, you'd reuse the same header file for both use-cases, and have a 'DLL' macro that switches between 'import' and 'export' for the function declarations.
I've had a quick look at Genny (https://github.com/treeform/genny) which seems very impressive, and the closest to what I'm looking for. However, as it's a third party library, and rather more sophisticated than what I'd anticipated needing, I wanted to quickly check if I was missing a simpler solution.
Thanks!
Simon
You can define your own pragma that works like the common DLL pragmas that windows programmers use in C:
from the "user defined pragma" section of the manual:
when appType == "lib":
{.pragma: rtl, exportc, dynlib, cdecl.}
else:
{.pragma: rtl, importc, dynlib: "client.dll", cdecl.}
proc p*(a, b: int): int {.rtl.} =
result = a + b
You may be able to just have the function body in both programs, I'm not sure what the compiler does when it sees {.importc, dynlib.} on a function that has a body, If it skips over the body then there's not really a great reason not to include the body.
If you want to have a nim module that acts as just a "header file" without the function bodies then maybe you could use a "macro pragma" that writes out the function declaration/prototype to a new file during compilation
I imagine you'd want to have the dynamic library shared with any user of the program and the "header" to be shared with people who want to compile it, but not share the implementation necessarily.
To riff off of what @Demos said you should be able to create a small macro which noted the signature of what was compiled and output a "header" file while attaching the correct pragmas. So you'd end up with one file in your project with all the implementations, then when you compile your dynamic library it would spit out the .so file and a .nim "header" file automatically generated from the macro invocations.
Somehow I failed to see this example, despite scouring the manual - thanks for pointing it out!
And indeed, as you and @PMunch suggest, I basically want to spit out an API header. Automating this with a macro sounds like a good approach - thanks!
In case it's helpful to someone else in the future, I ended up doing what @Demos suggested. It's a little hacked together, but seems to be working; I might put it into a proper shareable module at some point.
Essentially you use {.lib_api.}:
import std/macros
import std/tables
import std/paths
import std/dirs
import std/strutils
const exporting = defined(API_EXPORT)
var proc_exports {.compiletime.} = initTable[string, string]()
var type_exports {.compiletime.} : string
template export_stub* {.pragma.}
proc delete_members(node: var NimNode) =
var object_top = node.findChild(it.kind==nnkObjectTy)
if object_top == nil:
let ref_top = node.findChild(it.kind==nnkRefTy)
if ref_top != nil:
object_top = ref_top.findChild(it.kind==nnkObjectTy)
if object_top == nil:
error "This isn't an object"
let members_top = object_top.findChild(it.kind==nnkRecList)
members_top.del(1, members_top.len-1)
members_top[0] = newNilLit()
macro lib_api*(node: untyped): untyped =
result = node
# echo node.treeRepr
if exporting:
let moduleFile = lineInfoObj(node).filename
# let module = $ paths.extractFilename(moduleFile)
let module = moduleFile
case node.kind:
of nnkProcDef, nnkFuncDef:
let prefix: string =
if node.kind==nnkFuncDef:
"func "
else:
"proc "
let prag = pragma(node)
let prag_string = if prag != nil: "" else: prag.repr
if not prag_string.contains("cdecl"):
node.addPragma(newIdentNode("cdecl"))
if not prag_string.contains("exportc"):
node.addPragma(newIdentNode("exportc"))
if not prag_string.contains("dynlib"):
node.addPragma(newIdentNode("dynlib"))
let original_params = node.findChild(it.kind == nnkFormalParams)
assert original_params != nil
# let original_params_string = original_params.repr
let params = original_params.repr.replace(';', ',')
let procSig: string = prefix & node[0].repr & params & "{.cdecl,dynlib:dll_locator(),importc.}"
var exp: string = proc_exports.getOrDefault(module)
# echo "*** " & procSig
proc_exports[module] = exp & procSig & "\n"
of nnkTypeDef:
var objectTree = copyNimTree(node)
let prag = objectTree[0].findChild(it.kind == nnkPragma)
if prag != nil:
let stub = prag.findChild(it.kind == nnkIdent and eqIdent(it, "export_stub"))
if stub != nil:
delete_members(objectTree)
if objectTree[0][1].kind == nnkPragma:
let prag = objectTree[0][1]
let stub = prag.findChild(it.kind == nnkIdent and eqIdent(it, "export_stub"))
if stub != nil:
delete_members(objectTree)
# Remove the dodgy pragma from the export
objectTree[0][1] = newEmptyNode()
let typeSig:string = objectTree.repr
type_exports.add("type " & typeSig & "\n\n")
else:
discard
func generate_dll_locator(dll_name: string) : string =
result= """
import std/os
proc dll_locator*(): string =
result = "$1"
if fileExists(result): return
result = "$1.so"
if fileExists(result): return
result = "$1.dylib"
if fileExists(result): return
result = "$1.dll"
if fileExists(result): return
quit("could not load dynamic library")
""" % [dll_name]
macro write_api*(
api_root: static[string],
output_folder: static[string],
types_file: static[string],
dll_name: static[string],
) =
echo "EXPORTING!!"
let projectRoot = Path getProjectPath()
echo projectRoot.string
# let output_path = paths.absolutePath(Path output_folder, projectRoot)
# echo output_path.string
# let full_root = paths.absolutePath(Path api_root, )
dirs.createDir(Path output_folder)
let types_path : Path = Path(output_folder) / Path(types_file)
writeFile(types_path.string, type_exports)
let dll_path : Path = Path(output_folder) / Path("dll_locator.nim")
writeFile(dll_path.string, generate_dll_locator(dll_name))
for k, v in proc_exports:
echo "Root: " & projectRoot.string
echo "ModulePath: " & k
let module_path = Path k
let module_dir = paths.parentDir(module_path)
let module_file = paths.extractFilename(module_path)
echo "ModuleDir: " & module_dir.string
let module_subfolder_idx = module_dir.string.find(projectRoot.string)
assert(module_subfolder_idx >= 0)
let module_subfolder = module_dir.string[projectRoot.string.len() .. ^1]
echo "Subfolder: " & module_subfolder
let new_module_dir = Path(output_folder) / Path(module_subfolder)
let new_module_path = new_module_dir / module_file
echo "New module path " & new_module_path.string
# dirs.createDir(new_module_dir)
let contents =
"import " & paths.splitFile(Path types_file).name.string & "\n" &
"import dll_locator\n" &
v
writeFile(new_module_path.string, contents)
As @giaco15d says, we started going some of this with Genny: https://github.com/treeform/genny . Idea is to down-cast Nim's API into a plain C API, then up-cast it to some thing that is nice to use in Nim.
Use naming conventions that are familiar in the language: So callToNim turns to call_to_nim on the C side then back to callToNim on the nim side.
Make ref objects behave like OOP objects with members, methods and constructors. So a Nim ref objects gets converted to a sort of int handle when in C land then back to some thing that looks like ref object on the Nim side.
When you pass a seq, on the C side its treated as an int handle, but on the Nim side we expose the [], len and other seq calls.
For math stuff we overload math operators +, -, *, /. Although in C they look like vector_add, vector_sub etc...
When you pass an overloaded proc we convert it to several C procs: foo_int, foo_float, foo_string etc... but convert it back to just foo() on the nim side.
We even copy the comments so that automated tools can use them.
Oh an you get bindings to other langs: Python, C, Zig, Js, etc... as a bonus.
Thanks! Yes, Genny looks great (as do all the other Treeform packages actually - really impressive stuff!)
My needs are currently fairly simple, and I'm trying to minimise dependencies/complexity; but I'll be taking a proper look at Genny in the near future.
Thanks!