diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..155f12f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +nimcache +*.exe \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4c8051 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +Nimgen is a helper for [c2nim](https://github.com/nim-lang/c2nim/) to simpilfy and automate the wrapping of C libraries. + +Nimgen can be used to automate the process of manipulating C files so that c2nim can be run on them without issues. This includes adding/removing code snippets, removal of complex preprocessor definitions that c2nim doesn't yet comprehend and recursively running on #include files. + +__Installation__ + +Nimgen can be installed via [Nimble](https://github.com/nim-lang/nimble): + +```> nimble install https://github.com/genotrance/nimgen``` + +This will download, build and install nimgen in the standard Nimble package location, typically ~/.nimble. Once installed, it can be run just like c2nim. + +__Usage__ + +Nimgen is driven by a simple .cfg file that is read using the Nim [parsecfg](https://nim-lang.org/docs/parsecfg.html) module. The sections of the file are described further below. + +```> nimgen package.cfg``` + +A Nimble package for a library that is wrapped with nimgen will have the following:- + +* The .cfg file that tells nimgen what exactly to do +* Nimgen defined as a dependency defined in the .nimble file +* Steps within the .nimble file to download the source code that is being wrapped + +This way, the library source code doesn't need to get checked into the Nimble package and can evolve independently. + +Nimble already requires Git so those commands can be assumed to be present to download source from a repository. Mercurial is also suggested but depends on the user. Downloading arbitrary files depends on the OS. For Linux, wget/curl can be assumed. On Windows, [powershell](https://superuser.com/questions/362152/native-alternative-to-wget-in-windows-powershell) can be used. + +__Capabilities & Limitations__ + +Nimgen supports compiling in C/C++ sources as well as loading in dynamic libraries at this time. Support for static libraries (.a, .lib) are still to come. + +To see examples of nimgen in action, the nimssl library is a good example of C code getting compiled in whereas nimbass is an example of linking with a dynamic library. + +Nimgen only supports the ```gcc``` preprocessor at this time. Support for detecting and using other preprocessors is TBD. + +__Config file__ + +_[n.global]_ + +output = name of the Nimble project once installed, also location to place generated .nim files + +filter = string to identify and recurse into library .h files in #include statements and exclude standard headers + +_[n.include]_ + +List of all directories, one per line, to include in the search path. This is used by:- +* The preprocessor for #include files +* Nimgen to find #include files that are recursively processed + +Nimgen also adds {.passC.} declarations into the generated .nim files for these include paths if compiling source files directly. + +_[n.exclude]_ + +List of all directories or files to exclude from all parsing. If an entry here matches any portion of a file, it is excluded from recursive processing. + +_[sourcefile]_ + +The following keys apply to library source code and help with generating the .nim files. + +```recurse``` = find #include files and process them [default: false] + +```preprocess``` = run preprocessor (gcc -E) on file to remove #defines, etc. [default: false] - this is especially useful when c2nim doesn't support complex preprocessor usage + +```ctags``` = run ctags on file to filter out function definitions [default: false] - this requires the ctags executable and is an alternative to filter out preprocessor complexity + +```defines``` = pulls out simple #defines of ints, floats and hex values for separate conversion [default: false] - works only when preprocess or ctags is used and helps include useful definitions in generated .nim file + +```flags``` = flags to pass to the c2nim process in "quotes" [default: --stdcall]. --cdecl, --assumedef, --assumendef may be useful + +Multiple entries for the all following keys are possible by appending any .string to the key. E.g. dynlib.win, compile.dir + +```compile``` = file or dir of files of source code to {.compile.} into generated .nim + +```dynlib``` = dynamic library to load at runtime for generated .nim procs + +The following keys apply to library source code (before processing) and generated .nim files (after processing) and allow manipulating the files as required to enable successful wrapping. + +```create``` = create a file at exact location with contents specified + +```search``` = search string providing context for following prepend/append/replace directives + +```prepend``` = string value to prepend into file at beginning or before search + +```append``` = string value to append into file at the end or after search + +```replace``` = string value to replace search string in file + +__Feedback__ + +Nimgen is a work in progress and any feedback or suggestions are welcome. It is hosted on [GitHub](https://github.com/genotrance/nimgen) with an MIT license so issues, forks and PRs are most appreciated. diff --git a/nimgen.nim b/nimgen.nim new file mode 100644 index 0000000..06c95ca --- /dev/null +++ b/nimgen.nim @@ -0,0 +1,430 @@ +import nre +import os +import ospaths +import osproc +import parsecfg +import ropes +import sequtils +import streams +import strutils +import tables + +var FILES: TableRef[string, string] = newTable[string, string]() +var DONE: seq[string] = @[] + +var CONFIG: Config +var FILTER = "" +var OUTPUT = "" +var INCLUDES: seq[string] = @[] +var EXCLUDES: seq[string] = @[] + +# ### +# Helpers + +proc execProc(cmd: string): string = + var p = startProcess(cmd, options = {poStdErrToStdOut, poUsePath, poEvalCommand}) + + result = "" + var outp = outputStream(p) + var line = newStringOfCap(120).TaintedString + while true: + if outp.readLine(line): + result.add(line) + result.add("\n") + elif not running(p): break + + var x = p.peekExitCode() + if x != 0: + echo "Command failed: " & $x + echo cmd + echo result + quit() + +# ### +# File loction + +proc getnimout(file: string): string = + var nimout = file.splitFile().name & ".nim" + if OUTPUT != "": + nimout = OUTPUT/nimout + removeFile(nimout) + + return nimout + +proc exclude(file: string): bool = + for excl in EXCLUDES: + if excl in file: + return true + return false + +proc search(file: string): string = + if exclude(file): + return "" + + result = file + if file.splitFile().ext == ".nim": + result = getnimout(file) + elif not fileExists(result): + var found = false + for inc in INCLUDES: + result = inc/file + if fileExists(result): + found = true + break + if not found: + echo "File doesn't exist: " & file + quit() + + return result.replace(re"[\\/]", $DirSep) + +# ### +# Loading / unloading + +proc loadfile(file: string) = + if FILES.hasKey(file): + return + + FILES[file] = readFile(file) + +proc savefile(file: string) = + try: + if FILES.hasKey(file): + writeFile(file, FILES[file]) + + FILES.del(file) + except: + echo "Failed to save " & file + +proc savefiles() = + for file in FILES.keys(): + savefile(file) + +# ### +# Manipulating content + +proc prepend(file: string, data: string, search="") = + loadfile(file) + if search == "": + FILES[file] = data & FILES[file] + else: + let idx = FILES[file].find(search) + if idx != -1: + FILES[file] = FILES[file][0.."]""", "").strip() + if FILTER in inc and (not exclude(inc)): + result.add(inc) + + result = result.deduplicate() + +proc getdefines(file: string): string = + loadfile(file) + result = "" + for def in FILES[file].findIter(re"(?m)^(\s*#\s*define\s+[\w\d_]+\s+[\d.x]+)(?:\r|//|/*).*?$"): + result &= def.captures[0] & "\n" + +proc preprocess(file: string): string = + var cmd = "gcc -E " & file + for inc in INCLUDES: + cmd &= " -I " & inc + + # Run preprocessor + var data = execProc(cmd) + + # Include content only from file + var rdata: Rope + var start = false + let sfile = file.replace("\\", "/") + for line in data.splitLines(): + if line.strip() != "": + if line[0] == '#': + start = false + if sfile in line.replace("\\", "/").replace("//", "/"): + start = true + else: + if start: + rdata.add( + line.replace("_Noreturn", "") + .replace("WINAPI", "") + .replace("__attribute__", "") + .replace(re"\(\([_a-z]+?\)\)", "") + .replace(re"\(\(__format__\(__printf__, \d, \d\)\)\);", ";") & "\n" + ) + return $rdata + +proc ctags(file: string): string = + var cmd = "ctags -o - --fields=+S+K --c-kinds=p --file-scope=no " & file + var fps = execProc(cmd) + + var fdata = "" + for line in fps.splitLines(): + var spl = line.split(re"\t") + if spl.len() > 4: + if spl[0] != "main": + var fn = "" + var match = spl[2].find(re"/\^(.*?)\(") + if match.isSome(): + fn = match.get().captures[0] + fn &= spl[4].replace("signature:", "") & ";" + fdata &= fn & "\n" + + return fdata + +proc c2nim(fl, outfile, flags: string, recurse, preproc, ctag, define: bool, compile, dynlib: seq[string] = @[]) = + var file = search(fl) + if file == "": + return + + if file in DONE: + return + + echo "Processing " & file + DONE.add(file) + + fixfuncprotos(file) + + var incout = "" + if recurse: + var incls = getincls(file) + for inc in incls: + incout &= "import " & inc.splitFile().name & "\n" + c2nim(inc, getnimout(inc), flags, recurse, preproc, ctag, define) + + var cfile = file + if preproc: + cfile = "temp.c" + writeFile(cfile, preprocess(file)) + elif ctag: + cfile = "temp.c" + writeFile(cfile, ctags(file)) + + if define and (preproc or ctag): + prepend(cfile, getdefines(file)) + savefile(cfile) + + var extflags = "" + var passC = "" + var outlib = "" + if compile.len() != 0: + passC = "import strutils\n" + for inc in INCLUDES: + passC &= ("""{.passC: "-I\"" & gorge("nimble path $#").strip() & "/$#\"".}""" % [OUTPUT, inc]) & "\n" + passC &= "{.push importc.}\n{.push header: \"$#\".}\n" % fl + #extflags = "--header:\"$#\"" % fl + + if dynlib.len() != 0: + let win = "when defined(Windows):\n" + let lin = "when defined(Linux):\n" + let osx = "when defined(MacOSX):\n" + var winlib, linlib, osxlib: string = "" + for dl in dynlib: + if dl.splitFile().ext == ".dll": + winlib &= " const dynlib$# = \"$#\"\n" % [OUTPUT, dl] + if dl.splitFile().ext == ".so": + linlib &= " const dynlib$# = \"$#\"\n" % [OUTPUT, dl] + if dl.splitFile().ext == ".dylib": + osxlib &= " const dynlib$# = \"$#\"\n" % [OUTPUT, dl] + + if winlib != "": + outlib &= win & winlib & "\n" + if linlib != "": + outlib &= lin & linlib & "\n" + if winlib != "": + outlib &= osx & osxlib & "\n" + + if outlib != "": + extflags &= " --dynlib:dynlib$#" % OUTPUT + + # Run c2nim on generated file + var cmd = "c2nim $# $# --out:$# $#" % [flags, extflags, outfile, cfile] + when defined(windows): + cmd = "cmd /c " & cmd + discard execProc(cmd) + + if preproc: + try: + removeFile("temp.c") + except: + discard + + # Import nim modules + if recurse: + prepend(outfile, incout) + + # Nim doesn't like {.cdecl.} for type proc() + freplace(outfile, re"(?m)(.*? = proc.*?){.cdecl.}", "$#") + freplace(outfile, " {.cdecl.})", ")") + + # Include {.compile.} directives + for cpl in compile: + if getFileInfo(cpl).kind == pcFile: + prepend(outfile, compile(file=cpl)) + else: + prepend(outfile, compile(dir=cpl)) + + # Add header file and include paths + if passC != "": + prepend(outfile, passC) + append(outfile, "\n{.pop.}\n{.pop.}\n") + + # Add dynamic library + if outlib != "": + prepend(outfile, outlib) + +# ### +# Processor + +proc runcfg(cfg: string) = + if not fileExists(cfg): + echo "Config doesn't exist: " & cfg + quit() + + CONFIG = loadConfig(cfg) + + if CONFIG.hasKey("n.global"): + if CONFIG["n.global"].hasKey("output"): + OUTPUT = CONFIG["n.global"]["output"] + if CONFIG["n.global"].hasKey("filter"): + FILTER = CONFIG["n.global"]["filter"] + + if CONFIG.hasKey("n.include"): + for inc in CONFIG["n.include"].keys(): + INCLUDES.add(inc) + + if CONFIG.hasKey("n.exclude"): + for excl in CONFIG["n.exclude"].keys(): + EXCLUDES.add(excl) + + for file in CONFIG.keys(): + if file in @["n.global", "n.include", "n.exclude"]: + continue + + var sfile = search(file) + + var srch = "" + var action = "" + var compile: seq[string] = @[] + var dynlib: seq[string] = @[] + for act in CONFIG[file].keys(): + echo act + echo "A" & CONFIG[file][act] & "A" + action = act.replace(re"\..*", "") + if action == "create": + writeFile(file, CONFIG[file][act]) + elif action in @["prepend", "append", "replace", "compile", "dynlib"] and sfile != "": + if action == "prepend": + if srch != "": + prepend(sfile, CONFIG[file][act], CONFIG[file][srch]) + else: + prepend(sfile, CONFIG[file][act]) + elif action == "append": + if srch != "": + append(sfile, CONFIG[file][act], CONFIG[file][srch]) + else: + append(sfile, CONFIG[file][act]) + elif action == "replace": + if srch != "": + freplace(sfile, CONFIG[file][srch], CONFIG[file][act]) + elif action == "compile": + compile.add(CONFIG[file][act]) + elif action == "dynlib": + dynlib.add(CONFIG[file][act]) + srch = "" + elif action == "search": + srch = act + + if file.splitFile().ext != ".nim": + var recurse = false + var preproc = false + var ctag = false + var define = false + var flags = "--stdcall" + + # Save C files in case they have changed + savefile(sfile) + + for act in CONFIG[file].keys(): + if CONFIG[file][act] == "true": + if act == "recurse": + recurse = true + elif act == "preprocess": + preproc = true + elif act == "ctags": + ctag = true + elif act == "defines": + define = true + elif act == "flags": + flags = CONFIG[file][act] + + + c2nim(file, getnimout(file), flags, recurse, preproc, ctag, define, compile, dynlib) + +# ### +# Main loop + +if paramCount() == 0: + echo "nimgen file.cfg" + quit() + +for i in 1..paramCount(): + runcfg(paramStr(i)) + +savefiles() \ No newline at end of file diff --git a/nimgen.nimble b/nimgen.nimble new file mode 100644 index 0000000..d849bf5 --- /dev/null +++ b/nimgen.nimble @@ -0,0 +1,12 @@ +# Package + +version = "0.1.0" +author = "genotrance" +description = "c2nim helper to simpilfy and automate the wrapping of C libraries" +license = "MIT" + +# Dependencies + +requires "nim >= 0.16.0", "c2nim >= 0.9.13" + +bin = @["nimgen"] \ No newline at end of file