aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGanesh Viswanathan <dev@genotrance.com>2017-11-07 18:14:33 -0600
committerGanesh Viswanathan <dev@genotrance.com>2017-11-07 18:14:33 -0600
commit777ae910c88eeb4e4327a62c6e30514f197c2806 (patch)
tree897db822d3b0db7d628a9f02e57eaade35da43be
parentadd7bcfbe66d98fbb849ce6c6c89704c5655e29c (diff)
downloadnimgen-777ae910c88eeb4e4327a62c6e30514f197c2806.tar.gz
nimgen-777ae910c88eeb4e4327a62c6e30514f197c2806.zip
Initial version
-rw-r--r--.gitignore2
-rw-r--r--README.md91
-rw-r--r--nimgen.nim430
-rw-r--r--nimgen.nimble12
4 files changed, 535 insertions, 0 deletions
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..<idx] & data & FILES[file][idx..<FILES[file].len()]
+
+proc append(file: string, data: string, search="") =
+ loadfile(file)
+ if search == "":
+ FILES[file] &= data
+ else:
+ let idx = FILES[file].find(search)
+ let idy = idx + search.len()
+ if idx != -1:
+ FILES[file] = FILES[file][0..<idy] & data & FILES[file][idy..<FILES[file].len()]
+
+proc freplace(file: string, pattern: string, repl="") =
+ loadfile(file)
+ if pattern in FILES[file]:
+ FILES[file] = FILES[file].replace(pattern, repl)
+
+proc freplace(file: string, pattern: Regex, repl="") =
+ loadfile(file)
+ if FILES[file].find(pattern).isSome():
+ if "$#" in repl:
+ for m in FILES[file].findIter(pattern):
+ FILES[file] = FILES[file].replace(m.match, repl % m.captures[0])
+ else:
+ FILES[file] = FILES[file].replace(pattern, repl)
+
+proc compile(dir="", file=""): string =
+ proc fcompile(file: string): string =
+ return "{.compile: \"$#\".}" % file.replace("\\", "/")
+
+ var data = ""
+ if dir != "" and dirExists(dir):
+ for f in walkFiles(dir / "*.c"):
+ data &= fcompile(f) & "\n"
+
+ if file != "" and fileExists(file):
+ data &= fcompile(file) & "\n"
+
+ return data
+
+proc fixfuncprotos(file: string) =
+ var data = readFile(file)
+
+ var edit = false
+ for fp in data.findIter(re"(?m)(^.*?)[ ]*\(\*(.*?)\((.*?)\)\)[ \r\n]*\((.*?[\r\n]*.*?)\);"):
+ var tdout = "typedef $# (*type_$#)($#);\n" % [fp.captures[0], fp.captures[1], fp.captures[3]] &
+ "type_$# $#($#);" % [fp.captures[1], fp.captures[1], fp.captures[2]]
+ data = data.replace(fp.match, tdout)
+ edit = true
+
+ if edit:
+ writeFile(file, data)
+
+# ###
+# Convert to Nim
+
+proc getincls(file: string): seq[string] =
+ loadfile(file)
+ result = @[]
+ for f in FILES[file].findIter(re"(?m)^\s*#\s*include\s+(.*?)$"):
+ var inc = f.captures[0].replace(re"""[<>"]""", "").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