diff options
| -rw-r--r-- | README.md | 16 | ||||
| -rw-r--r-- | nimterop/build.nim | 50 | ||||
| -rw-r--r-- | nimterop/cimport.nim | 1 | ||||
| -rw-r--r-- | nimterop/docs.nim | 5 | ||||
| -rw-r--r-- | nimterop/nimconf.nim | 199 | ||||
| -rw-r--r-- | tests/getheader.nims | 1 |
6 files changed, 238 insertions, 34 deletions
@@ -12,7 +12,7 @@ The nimterop wrapping functionality is still limited to C but is constantly expa Nimterop has seen some adoption within the community and the simplicity and success of this approach justifies additional investment of time and effort. Regardless, the goal is to make interop seamless so nimterop will focus on wrapping headers and not the outright conversion of C/C++ implementation. -### Installation +## Installation Nimterop can be installed via [Nimble](https://github.com/nim-lang/nimble): @@ -28,11 +28,11 @@ nimble build This will download and install nimterop in the standard Nimble package location, typically `~/.nimble`. Once installed, it can be imported into any Nim program. -### Usage +## Usage Detailed documentation can be found [here](https://nimterop.github.io/nimterop/theindex.html). Also, check out the [wiki](https://github.com/nimterop/nimterop/wiki/Wrappers) for a list of all known wrappers that have been created using nimterop. They will provide real world examples of how to wrap libraries. Please do add your project once you are done so that others can benefit from your work. -#### Build API +### Build API Creating a wrapper has two parts, the first is to setup the C library. This includes downloading it or finding it if already installed, and building it if applicable. The `getHeader()` high-level API provides all of this functionality as a convenience. Following is an example of using the high-level `getHeader()` API to perform all building and linking automatically: @@ -103,7 +103,7 @@ The `-d:xxxYYY` Nim define flags have already been described above and can be sp If more fine-tuned control is desired over the build process, it is possible to manually control all steps that `getHeader()` performs by directly using the API provided by [build](https://nimterop.github.io/nimterop/build.html). Note also that there is no requirement to use these APIs to setup the library. Any other established mechanisms can be used to do so any limitations imposed by Nimterop are unintentional and feedback is most welcome. -#### Wrapper API +### Wrapper API Once the C library is setup, the next step is to create wrappers that inform Nim of all the types and functions that are available. Following is a simple example covering the API: @@ -152,7 +152,7 @@ __Compiling source__ The job of building and compiling the underlying C library is best left to the build mechanism selected by the library author so using `getHeader()` is recommended. For simpler projects with a few `.c` files though, `cCompile()` should be more than enough. It is not recommended for larger projects which heavily rely on functionality offered by build tools. Recreating reliable logic in Nim can be tedious and one can expect minimal support from that author if their tested build mechanism is not used. -#### Command line API +### Command line API The `toast` binary can also be used directly on the CLI, similar to `c2nim`. The `cPlugin()` interface @@ -190,7 +190,7 @@ Options: -O=, --symOverride= strings {} skip generating specified symbols ``` -### Why nimterop +## Why nimterop Nim has one of the best FFI you can find - importing C/C++ is supported out of the box. All you need to provide is type and proc definitions for Nim to interop with C/C++ binaries. Generation of these wrappers is easy for simple libraries but can quickly get out of hand. [c2nim](https://github.com/nim-lang/c2nim) greatly helps here by parsing and converting C/C++ into Nim but is limited due to the complex and constantly evolving C/C++ grammar. [nimgen](https://github.com/genotrance/nimgen) mainly focused on automating the wrapping process with `c2nim` and filled some holes but is again limited to `c2nim` capabilities. @@ -212,10 +212,10 @@ The con of this approach of delegating to the preprocessor is that the Nim wrapp This is part of the reason why Nimterop provides a wrapper API so that the generation of wrappers is Nim code that can be rendered as part of the build process on the target platform. It helps to think of Nimterop as a build time tool like `cmake` that renders artifacts on the target rather than a tool whose generated artifacts should be checked into source control. Regardless, both the wrapper API and the `toast` command line still allow saving the wrapper output to a file to be stored in source control since it might work well enough for many projects. -__Credits__ +## Credits Nimterop depends on [tree-sitter](http://tree-sitter.github.io/tree-sitter/) and all licensing terms of [tree-sitter](https://github.com/tree-sitter/tree-sitter/blob/master/LICENSE) apply to the usage of this package. The tree-sitter functionality is pulled and wrapped using nimterop itself. -__Feedback__ +## Feedback Nimterop is a work in progress and any feedback or suggestions are welcome. It is hosted on [GitHub](https://github.com/nimterop/nimterop) with an MIT license so issues, forks and PRs are most appreciated. diff --git a/nimterop/build.nim b/nimterop/build.nim index a025dc0..e11cd7b 100644 --- a/nimterop/build.nim +++ b/nimterop/build.nim @@ -4,11 +4,32 @@ import os except findExe, sleep import regex +proc fixCmd(cmd: string): string = + when defined(Windows): + # Replace 'cd d:\abc' with 'd: && cd d:\abc` + var filteredCmd = cmd + if cmd.toLower().startsWith("cd"): + var + colonIndex = cmd.find(":") + driveLetter = cmd.substr(colonIndex-1, colonIndex) + if (driveLetter[0].isAlphaAscii() and + driveLetter[1] == ':' and + colonIndex == 4): + filteredCmd = &"{driveLetter} && {cmd}" + result = "cmd /c " & filteredCmd + elif defined(posix): + result = cmd + else: + doAssert false + proc sanitizePath*(path: string, noQuote = false, sep = $DirSep): string = result = path.multiReplace([("\\\\", sep), ("\\", sep), ("/", sep)]) if not noQuote: result = result.quoteShell +# Nim cfg file related functionality +include "."/nimconf + proc sleep*(milsecs: int) = ## Sleep at compile time let @@ -20,14 +41,9 @@ proc sleep*(milsecs: int) = discard gorgeEx(cmd & $(milsecs / 1000)) -proc getOsCacheDir(): string = - when defined(posix): - result = getEnv("XDG_CACHE_HOME", getHomeDir() / ".cache") / "nim" - else: - result = getHomeDir() / "nimcache" - proc getNimteropCacheDir(): string = - result = getOsCacheDir() / "nimterop" + # Get location to cache all nimterop artifacts + result = getNimcacheDir() / "nimterop" proc getCurrentNimCompiler*(): string = result = getCurrentCompilerExe() @@ -46,24 +62,8 @@ proc execAction*(cmd: string, retry = 0, die = true, cache = false, ## `die = false` - return on errors ## `cache = true` - cache results unless cleared with -f ## `cacheKey` - key to create unique cache entry - var - ccmd = "" - when defined(Windows): - # Replace 'cd d:\abc' with 'd: && cd d:\abc` - var filteredCmd = cmd - if cmd.toLower().startsWith("cd"): - var - colonIndex = cmd.find(":") - driveLetter = cmd.substr(colonIndex-1, colonIndex) - if (driveLetter[0].isAlphaAscii() and - driveLetter[1] == ':' and - colonIndex == 4): - filteredCmd = &"{driveLetter} && {cmd}" - ccmd = "cmd /c " & filteredCmd - elif defined(posix): - ccmd = cmd - else: - doAssert false + let + ccmd = fixCmd(cmd) when nimvm: # Cache results for speedup if cache = true diff --git a/nimterop/cimport.nim b/nimterop/cimport.nim index 3e58bc8..4afb27c 100644 --- a/nimterop/cimport.nim +++ b/nimterop/cimport.nim @@ -286,6 +286,7 @@ proc cPluginHelper(body: string, imports = "import macros, nimterop/plugin\n\n") if not fileExists(path) or gStateCT.nocache or compileOption("forceBuild"): mkDir(path.parentDir()) writeFile(path, data) + writeNimConfig(path & ".cfg") doAssert fileExists(path), "Unable to write plugin file: " & path diff --git a/nimterop/docs.nim b/nimterop/docs.nim index 97fab96..6eb812b 100644 --- a/nimterop/docs.nim +++ b/nimterop/docs.nim @@ -2,8 +2,11 @@ import macros, strformat from os import parentDir, getCurrentCompilerExe, DirSep +when defined(nimdoc) or (NimMajor, NimMinor) >= (1, 3): + from os import paramCount, paramStr + when defined(nimdoc): - from os import getCurrentDir, paramCount, paramStr + from os import getCurrentDir proc getNimRootDir(): string = #[ diff --git a/nimterop/nimconf.nim b/nimterop/nimconf.nim new file mode 100644 index 0000000..fdf2648 --- /dev/null +++ b/nimterop/nimconf.nim @@ -0,0 +1,199 @@ +import json, macros, os, osproc, sets, strformat, strutils + +when nimvm: + when (NimMajor, NimMinor, NimPatch) >= (1, 2, 0): + import std/compilesettings +else: + discard + +# Config detected with std/compilesettings or `nim dump` +type + Config* = ref object + NimMajor*: int + NimMinor*: int + NimPatch*: int + + paths*: OrderedSet[string] + nimblePaths*: OrderedSet[string] + nimcacheDir*: string + +proc getJson(projectDir: string): JsonNode = + # Get `nim dump` json value for `projectDir` + var + cmd = "nim --hints:off --dump.format:json dump dummy" + dump = "" + ret = 0 + + if projectDir.len != 0: + # Run `nim dump` in `projectDir` if specified + cmd = &"cd {projectDir.sanitizePath} && " & cmd + + cmd = fixCmd(cmd) + when nimvm: + (dump, ret) = gorgeEx(cmd) + else: + (dump, ret) = execCmdEx(cmd) + + try: + result = parseJson(dump) + except JsonParsingError as e: + echo "# Failed to parse `nim dump` output: " & e.msg + +proc getOsCacheDir(): string = + # OS default cache directory + when defined(posix): + result = getEnv("XDG_CACHE_HOME", getHomeDir() / ".cache") / "nim" + else: + result = getHomeDir() / "nimcache" + +proc getProjectDir*(): string = + ## Get project directory for this compilation - returns `""` at runtime + when nimvm: + when (NimMajor, NimMinor, NimPatch) >= (1, 2, 0): + # If nim v1.2.0+, get from `std/compilesettings` + result = querySetting(projectFull).parentDir() + else: + # Get from `macros` + result = getProjectPath() + else: + discard + +proc getNimcacheDir*(projectDir = ""): string = + ## Get nimcache directory for current compilation or specified `projectDir` + when nimvm: + when (NimMajor, NimMinor, NimPatch) >= (1, 2, 0): + # Get value at compile time from `std/compilesettings` + result = querySetting(SingleValueSetting.nimcacheDir) + else: + discard + + # Not Nim v1.2.0+ or runtime + if result.len == 0: + let + # Get project directory for < v1.2.0 at compile time + projectDir = if projectDir.len != 0: projectDir else: getProjectDir() + + # Use `nim dump` to figure out nimcache for `projectDir` + let + dumpJson = getJson(projectDir) + + if dumpJson != nil and dumpJson.hasKey("nimcache"): + result = dumpJson["nimcache"].getStr() + let + (head, tail) = result.splitPath() + if "dummy" in tail: + # Remove `dummy_d` subdir when default nimcache + result = head + + # Set to OS defaults if not detectable + if result.len == 0: + result = getOsCacheDir() + +proc jsonToSeq(node: JsonNode, key: string): seq[string] = + # Convert JsonArray to seq[string] for specified `key` + if node.hasKey(key): + for elem in node[key].getElems(): + result.add elem.getStr() + +proc getNimConfig*(projectDir = ""): Config = + # Get `paths` - list of paths to be forwarded to Nim + result = new(Config) + var + libPath, version: string + lazyPaths, searchPaths: seq[string] + + when nimvm: + result.NimMajor = NimMajor + result.NimMinor = NimMinor + result.NimPatch = NimPatch + + when (NimMajor, NimMinor, NimPatch) >= (1, 2, 0): + # Get value at compile time from `std/compilesettings` + libPath = getCurrentCompilerExe().parentDir().parentDir() / "lib" + lazyPaths = querySettingSeq(MultipleValueSetting.lazyPaths) + searchPaths = querySettingSeq(MultipleValueSetting.searchPaths) + result.nimcacheDir = querySetting(SingleValueSetting.nimcacheDir) + else: + discard + + let + # Get project directory for < v1.2.0 at compile time + projectDir = if projectDir.len != 0: projectDir else: getProjectDir() + + # Not Nim v1.2.0+ or runtime + if libPath.len == 0: + let + dumpJson = getJson(projectDir) + + if dumpJson != nil: + if dumpJson.hasKey("version"): + version = dumpJson["version"].getStr() + lazyPaths = jsonToSeq(dumpJson, "lazyPaths") + searchPaths = jsonToSeq(dumpJson, "lib_paths") + if dumpJson.hasKey("libpath"): + libPath = dumpJson["libpath"].getStr() + elif searchPaths.len != 0: + # Usually `libPath` is last entry in `searchPaths` + libPath = searchPaths[^1] + + # Parse version + if version.len != 0: + let + splversion = version.split({'.'}, maxsplit = 3) + result.NimMajor = splversion[0].parseInt() + result.NimMinor = splversion[1].parseInt() + result.NimPatch = splversion[2].parseInt() + + # Find non standard lib paths added to `searchPath` + for path in searchPaths: + if libPath notin path: + result.paths.incl path + + # Find `nimblePaths` in `lazyPaths` + for path in lazyPaths: + let + (_, tail) = path.strip(leading = false, chars = {'/', '\\'}).splitPath() + if tail == "pkgs": + # Nimble path probably + result.nimblePaths.incl path + + # Find `paths` in `lazyPaths` that aren't within `nimblePaths` + # Have to do this separately since `nimblePaths` could be after + # packages in `lazyPaths` + for path in lazyPaths: + var skip = false + for npath in result.nimblePaths: + if npath in path: + skip = true + break + if not skip: + result.paths.incl path + + result.nimcacheDir = getNimcacheDir(projectDir) + +proc writeNimConfig*(cfg: Config, cfgFile: string) = + # Write Nim configuration to file + var + cfgOut = &"--nimcache:\"{cfg.nimcacheDir}\"\n" + + if (cfg.NimMajor, cfg.NimMinor, cfg.NimPatch) >= (1, 2, 0): + # --clearNimbleCache if Nim v1.2.0+ + cfgOut &= "--clearNimblePath\n" + + # Add `nimblePaths` if detected - v1.2.0+ + for path in cfg.nimblePaths: + cfgOut &= &"--nimblePath:\"{path}\"\n" + + # Add `paths` in all cases if any detected + for path in cfg.paths: + cfgOut &= &"--path:\"{path}\"\n" + + when defined(windows): + cfgOut = cfgOut.replace("\\", "/") + + writeFile(cfgFile, cfgOut) + +proc writeNimConfig*(cfgFile: string, projectDir = "") = + let + cfg = getNimConfig(projectDir) + writeNimConfig(cfg, cfgFile) diff --git a/tests/getheader.nims b/tests/getheader.nims index 7a0fc80..ef501ab 100644 --- a/tests/getheader.nims +++ b/tests/getheader.nims @@ -11,6 +11,7 @@ proc testCall(cmd, output: string, exitCode: int, delete = true) = if not delete: ccmd = ccmd.replace(" -f ", " ") + echo ccmd var (outp, exitC) = gorgeEx(ccmd) echo outp |
