diff --git a/build.sbt b/build.sbt index ed8a538..4034e7a 100644 --- a/build.sbt +++ b/build.sbt @@ -138,23 +138,52 @@ lazy val sbtPlugin = project("sbt-scala-native-bindgen", ScriptedPlugin) publishLocal := publishLocal.dependsOn(tools / publishLocal).value ) +lazy val docsUsingBindingsDirectory = settingKey[File]("vector source") +lazy val docs3rdPartyBindingsDirectory = settingKey[File]("geometry source") +lazy val docsScalaNativeBindingsDirectory = settingKey[File]("wordcount source") + lazy val docs = nativeProject("docs") .enablePlugins(GhpagesPlugin, ParadoxSitePlugin, ParadoxMaterialThemePlugin) .enablePlugins(ScalaNativeBindgenPlugin) .settings( publish / skip := true, - Test / nativeBindings += { - NativeBinding((Test / resourceDirectory).value / "vector.h") + docsUsingBindingsDirectory := (Test / resourceDirectory).value / "using-bindings", + docs3rdPartyBindingsDirectory := (Test / resourceDirectory).value / "3rd-party-bindings", + docsScalaNativeBindingsDirectory := (Test / resourceDirectory).value / "scala-native-bindings", + Test / nativeBindings ++= Seq( + NativeBinding(docsUsingBindingsDirectory.value / "vector.h") .name("vector") .link("vector") - .packageName("org.example") - }, + .packageName("org.example"), { + val pathToHeader = docs3rdPartyBindingsDirectory.value / "geometry.h" + val pathToConfig = docs3rdPartyBindingsDirectory.value / "config.json" + //#sbt-binding-config + NativeBinding(pathToHeader) + .bindingConfig(pathToConfig) + //#sbt-binding-config + .name("Geometry") + .link("geometry") + .packageName("org.example.geometry") + }, { + val pathToHeader = docsScalaNativeBindingsDirectory.value / "wordcount.h" + //#sbt-exclude-prefix + NativeBinding(pathToHeader) + .excludePrefix("__") + //#sbt-exclude-prefix + .name("WordCount") + .link("wordcount") + .packageName("org.example.wordcount") + .bindingConfig(docsScalaNativeBindingsDirectory.value / "config.json") + } + ), nativeBindgenPath := { Some( (ThisBuild / baseDirectory).value / "bindgen/target/scala-native-bindgen") }, Test / nativeBindgen / target := (Test / scalaSource).value / "org/example", - compileTask("vector", Test / resourceDirectory), + compileTask("vector", docsUsingBindingsDirectory), + compileTask("geometry", docs3rdPartyBindingsDirectory), + compileTask("wordcount", docsScalaNativeBindingsDirectory), libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.0-SNAP10" % Test, Paradox / paradoxProperties ++= Map( "github.base_url" -> scmInfo.value.get.browseUrl.toString diff --git a/docs/src/paradox/cli.md b/docs/src/paradox/cli.md index 9d3c39c..6178744 100644 --- a/docs/src/paradox/cli.md +++ b/docs/src/paradox/cli.md @@ -45,11 +45,11 @@ The generated bindings can be configured using the different options and it is a | Option | Description |----------------------|---------------------------------------------------------------------------------| -| `--link` | Library to link with, e.g. `--link` uv for libuv. +| `--link` | Library to link with, e.g. `--link uv` for libuv. | `--no-link` | Library does not require linking. -| `--name` | Scala object name that contains bindings. Default value set to library name. +| `--name` | Scala object name that contains bindings. Defaults to the library name. | `--package` | Package name of generated Scala file. -| `--exclude-prefix` | Functions and unused typedefs will be removed if their names have given prefix. -| `--binding-config` | Path to a config file that contains the information about bindings that should be reused. See @ref:[Integrating Bindings](integrating-bindings.md) for more information. +| `--exclude-prefix` | Functions and unused typedefs will be removed if their names have the given prefix. +| `--binding-config` | Path to a config file that contains the information about bindings that should be reused. See @ref:[Configuration](configuration.md) for more information. | `--extra-arg` | Additional argument to append to the compiler command line. | `--extra-arg-before` | Additional argument to prepend to the compiler command line. diff --git a/docs/src/paradox/configuration.md b/docs/src/paradox/configuration.md new file mode 100644 index 0000000..03cff95 --- /dev/null +++ b/docs/src/paradox/configuration.md @@ -0,0 +1,94 @@ +# Configuration + +Bindgen provides several options for configuring what to include and how +to output the generated bindings. The options can be provided to the CLI +or as part of the sbt bindings declaration. + +@@ toc + +## Excluding Definitions By Prefix + +Definitions may be excluded by their prefix. This is useful when private definitions should not be part of the generated binding. This is often the case for definitions starting with `__`. + +sbt +: @@snip [sbt] (../../../build.sbt) { #sbt-exclude-prefix } + +CLI +: ```sh + scala-native-bindgen --binding-config=/path/to/config + ``` + +To exemplify consider the following header file: + +@@snip [sbt] (../../../sbt-scala-native-bindgen/src/sbt-test/bindgen/generate/src/main/resources/stdlib.h) + +When the exclude prefix is set to `__`, then the resulting bindings will be: + +@@snip [sbt] (../../../sbt-scala-native-bindgen/src/sbt-test/bindgen/generate/expected/stdlib.scala) + +## Binding Configuration File + +The binding configuration is a JSON file which allows to map the path of +a header file to the associated object as well as the names of the C +types and symbols to their respective Scala types and definitions. The +configuration file can be used when integrating with third party +bindings. + +sbt +: @@snip [sbt-binding-config](../../../build.sbt) { #sbt-binding-config } + +CLI +: ```sh + scala-native-bindgen --binding-config=/path/to/config + ``` + +### Using Types From Third Party Bindings + +If you need to generate bindings that uses types from bindings that have not been generated with `scala-native-bindgen` you have to provide the mapping between the header path and the Scala object's fully qualified name. And in some cases also the Scala name of the C types. Using the vector library example from @ref:[Using Bindings](using-bindings.md), let's assume that the vector library was created so that `struct point` was named `Point`: + +@@snip [Vector] (../test/scala/com/example/custom/binding/Vector.scala) { #example } + +To use this binding, create a configuration file with the folllowing content, where `path` is the name of the header file (usually the part of the path inside the `/usr/include` or `/usr/local/include` directory), `object` is the fully qualified name of the Scala object (i.e. package name as well as the Scala object name) and finally `names` for each of the types: + +@@snip [vector.h] (../test/resources/3rd-party-bindings/config.json) + +Now in the library you are creating a binding for, any usage of `struct point`: + +@@snip [vector.h] (../test/resources/3rd-party-bindings/geometry.h) { #using-struct-point } + +will reference the `Point` type alias inside the `Vector` object: + +@@snip [Geometry] (../test/scala/org/example/Geometry.scala) { #using-struct-point } + +### Using Types From the Scala Native Bindings + +Similar to the above, the following example shows how you can use +types defined in the [C standard library] and [C POSIX library] bindings +shipped with Scala Native. Let's assume we have a binding with a method that uses the `FILE` type +from ``: + +@@snip [vector.h] (../test/resources/scala-native-bindings/wordcount.h) { #using-stdio-file } + +We can then write a binding configuration that maps the header name to the object defined in Scala Native. + +@@snip [vector.h] (../test/resources/scala-native-bindings/config.json) + +@@@ note + +In the above binding configuration, we duplicate the mapping to work on both Linux and macOS since on macOS +the definition of `FILE` is found inside `/usr/include/_stdio.h`. + +@@@ + +The generated binding will then use the `stdio.h` binding provided by Scala Native: + +@@snip [WordCount] (../test/scala/org/example/WordCount.scala) { #using-stdio-file } + +And we can use the binding by opening a file using `fopen(...)`: + +@@snip [WordCountSpec] (../test/scala/org/scalanative/bindgen/docs/WordCountSpec.scala) { #example } + + [Scala Native memory management]: http://www.scala-native.org/en/latest/user/interop.html#memory-management + [Scala Native memory layout types]: http://www.scala-native.org/en/latest/user/interop.html#memory-layout-types + [C standard library]: http://www.scala-native.org/en/latest/lib/libc.html + [C POSIX library]: http://www.scala-native.org/en/latest/lib/posixlib.html diff --git a/docs/src/paradox/index.md b/docs/src/paradox/index.md index a5c5212..9794ce9 100644 --- a/docs/src/paradox/index.md +++ b/docs/src/paradox/index.md @@ -29,8 +29,8 @@ This project is distributed under the Scala license. * [sbt](sbt.md) * [cli](cli.md) -* [Using Generated Bindings](using-generated-bindings.md) -* [Integrating Bindings](integrating-bindings.md) +* [usage](using-bindings.md) +* [configuration](configuration.md) * [Limitations](limitations.md) * [bindings](bindings/index.md) * [Contributor's Guide](contrib/index.md) diff --git a/docs/src/paradox/integrating-bindings.md b/docs/src/paradox/integrating-bindings.md deleted file mode 100644 index f07d164..0000000 --- a/docs/src/paradox/integrating-bindings.md +++ /dev/null @@ -1,27 +0,0 @@ -# Integrating Bindings - -To reuse already generated bindings create a `config.json` file that defines which headers correspond to which Scala objects: - -```json -{ - "_stdio.h": "scala.scalanative.native.stdio", - "/full/path/to/regexp4.h": "bindings.regexp4" -} -``` - -Bindgen assumes that type names in header files match type names in generated binding (spaces in struct, union and enum -names are replaces with underscores), but it is possible to specify custom names mapping: - -```json -{ - "hiredis.h": { - "object": "bindings.hiredis.hiredis", - "names": { - "redisContext": "Context" - } - } -} -``` - -Provide a path to `config.json` to bindgen using `--binding-config` command-line option or `NativeBinding.bindingConfig` -sbt plugin option (see @ref:[Using the sbt plugin](sbt.md)). diff --git a/docs/src/paradox/sbt.md b/docs/src/paradox/sbt.md index 358811f..fddbb67 100644 --- a/docs/src/paradox/sbt.md +++ b/docs/src/paradox/sbt.md @@ -28,6 +28,6 @@ Given that the `stdlib.h` header file contains: @@snip [sbt] (../../../sbt-scala-native-bindgen/src/sbt-test/bindgen/generate/src/main/resources/stdlib.h) -Running `nativeBindgen` will generate a file named `target/scala-2.11/src_managed/main/sbt-scala-native-bindgen/stdlib.scala` containing something along the following lines: +Running `nativeBindgen` will generate a file named `target/scala-2.11/src_managed/main/sbt-scala-native-bindgen/stdlib.scala` containing the following lines: @@snip [sbt] (../../../sbt-scala-native-bindgen/src/sbt-test/bindgen/generate/expected/stdlib.scala) \ No newline at end of file diff --git a/docs/src/paradox/using-generated-bindings.md b/docs/src/paradox/using-bindings.md similarity index 74% rename from docs/src/paradox/using-generated-bindings.md rename to docs/src/paradox/using-bindings.md index f04d675..d9727ea 100644 --- a/docs/src/paradox/using-generated-bindings.md +++ b/docs/src/paradox/using-bindings.md @@ -1,23 +1,30 @@ -# Using Generated Bindings +# Using Bindings + +The following explains how to use bindings generated with Scala Native Bindgen through several examples. + +## A Simple Vector Library Consider following header file: -@@snip [vector.h] (../test/resources/vector.h) +@@snip [vector.h] (../test/resources/using-bindings/vector.h) Bindgen will output + * type aliases for the structs * binding for function `cosine` * helper functions that make usage of structs easier @@snip [vector.h] (../test/scala/org/example/vector.scala) -Let's write code that creates two line segments and prints angel between them. +## Using the Vector Library + +Let's write code that creates two line segments and calculates the angel between them. First we need to create points. We will use `native.Zone` to allocate struct (more information on memory management can be found here: [Scala Native memory management]). -Generated bindings contain helper functions that make struct allocation easier. +The generated bindings contain helper functions that make struct allocation easier. To import them use `import org.example.vector._` Let's create two points and the first line segment: @@ -39,17 +46,16 @@ to your code: @@@ note -Note that `struct_lineSegment` contains fields of value type `struct_point` +`struct_lineSegment` contains fields of value type `struct_point` but setters accept variables of type `native.Ptr[struct_point]`. -It helps to avoid Scala Native limitation that does not allow passing structs +The reason is that Scala Native does not allow passing structs and arrays by value (see @github[scala-native/scala-native#555](scala-native/scala-native#555)). @@@ -Now we can calculate angel between line segments: +Now we can calculate the angel between the line segments: @@snip [step-3] (../test/scala/org/scalanative/bindgen/docs/VectorSpec.scala) { #step-3 } - [Scala Native memory management]: http://www.scala-native.org/en/latest/user/interop.html#memory-management [Scala Native memory layout types]: http://www.scala-native.org/en/latest/user/interop.html#memory-layout-types diff --git a/docs/src/test/resources/3rd-party-bindings/config.json b/docs/src/test/resources/3rd-party-bindings/config.json new file mode 100644 index 0000000..7cb53a4 --- /dev/null +++ b/docs/src/test/resources/3rd-party-bindings/config.json @@ -0,0 +1,9 @@ +{ + "vector.h": { + "object": "com.example.custom.binding.Vector", + "names": { + "struct point": "Point", + "struct lineSegment": "LineSegment" + } + } +} \ No newline at end of file diff --git a/docs/src/test/resources/3rd-party-bindings/geometry.c b/docs/src/test/resources/3rd-party-bindings/geometry.c new file mode 100644 index 0000000..aee5f43 --- /dev/null +++ b/docs/src/test/resources/3rd-party-bindings/geometry.c @@ -0,0 +1,6 @@ +#include "geometry.h" +#include + +float circle_area(struct circle *circle) { + return pow(circle->radius, 2) * M_PI; +} diff --git a/docs/src/test/resources/3rd-party-bindings/geometry.h b/docs/src/test/resources/3rd-party-bindings/geometry.h new file mode 100644 index 0000000..c4e2285 --- /dev/null +++ b/docs/src/test/resources/3rd-party-bindings/geometry.h @@ -0,0 +1,10 @@ +#include "../using-bindings/vector.h" + +//#using-struct-point +struct circle { + struct point point; + double radius; +}; +//#using-struct-point + +float circle_area(struct circle *circle); diff --git a/docs/src/test/resources/scala-native-bindings/config.json b/docs/src/test/resources/scala-native-bindings/config.json new file mode 100644 index 0000000..58948a7 --- /dev/null +++ b/docs/src/test/resources/scala-native-bindings/config.json @@ -0,0 +1,8 @@ +{ + "stdio.h": { + "object": "scala.scalanative.native.stdio" + }, + "_stdio.h": { + "object": "scala.scalanative.native.stdio" + } +} \ No newline at end of file diff --git a/docs/src/test/resources/scala-native-bindings/wordcount.c b/docs/src/test/resources/scala-native-bindings/wordcount.c new file mode 100644 index 0000000..69e7550 --- /dev/null +++ b/docs/src/test/resources/scala-native-bindings/wordcount.c @@ -0,0 +1,39 @@ +#include "wordcount.h" +#include +#include + +int wordcount(struct wordcount *wordcount, FILE *file) { + char buf[4096]; + + // FIXME: This doesn't seem to have any effect + // memset(wordcount, sizeof(*wordcount), 0); + + wordcount->chars = 0; + wordcount->words = 0; + wordcount->lines = 0; + + while (fgets(buf, sizeof(buf), file)) { + char *pos; + int in_word = 0; + + wordcount->lines++; + wordcount->chars += strlen(buf); + + for (pos = buf; *pos; pos++) { + if (isspace(*pos)) { + if (in_word) { + wordcount->words++; + } + in_word = 0; + } else { + in_word = 1; + } + } + + if (in_word) { + wordcount->words++; + } + } + + return 0; +} diff --git a/docs/src/test/resources/scala-native-bindings/wordcount.h b/docs/src/test/resources/scala-native-bindings/wordcount.h new file mode 100644 index 0000000..dbd427e --- /dev/null +++ b/docs/src/test/resources/scala-native-bindings/wordcount.h @@ -0,0 +1,11 @@ +#include + +struct wordcount { + long chars; + long lines; + long words; +}; + +//#using-stdio-file +int wordcount(struct wordcount *wordcount, FILE *file); +//#using-stdio-file diff --git a/docs/src/test/resources/vector.c b/docs/src/test/resources/using-bindings/vector.c similarity index 100% rename from docs/src/test/resources/vector.c rename to docs/src/test/resources/using-bindings/vector.c diff --git a/docs/src/test/resources/vector.h b/docs/src/test/resources/using-bindings/vector.h similarity index 100% rename from docs/src/test/resources/vector.h rename to docs/src/test/resources/using-bindings/vector.h diff --git a/docs/src/test/scala/com/example/custom/binding/Vector.scala b/docs/src/test/scala/com/example/custom/binding/Vector.scala new file mode 100644 index 0000000..e09a104 --- /dev/null +++ b/docs/src/test/scala/com/example/custom/binding/Vector.scala @@ -0,0 +1,61 @@ +//#example +package com.example.custom.binding + +import scala.scalanative._ +import scala.scalanative.native._ + +@native.link("vector") +@native.extern +object Vector { + type Point = native.CStruct2[native.CFloat, native.CFloat] + type LineSegment = native.CStruct2[Point, Point] + // ... + //#example + def cosine(v1: native.Ptr[LineSegment], + v2: native.Ptr[LineSegment]): native.CFloat = native.extern + + object implicits { + implicit class PointOps(val p: native.Ptr[Point]) extends AnyVal { + def x: native.CFloat = !p._1 + def x_=(value: native.CFloat): Unit = !p._1 = value + def y: native.CFloat = !p._2 + def y_=(value: native.CFloat): Unit = !p._2 = value + } + + implicit class LineSegmentOps(val p: native.Ptr[LineSegment]) + extends AnyVal { + def a: native.Ptr[Point] = p._1 + def a_=(value: native.Ptr[Point]): Unit = !p._1 = !value + def b: native.Ptr[Point] = p._2 + def b_=(value: native.Ptr[Point]): Unit = !p._2 = !value + } + } + + object Point { + import implicits._ + def apply()(implicit z: native.Zone): native.Ptr[Point] = + native.alloc[Point] + def apply(x: native.CFloat, y: native.CFloat)( + implicit z: native.Zone): native.Ptr[Point] = { + val ptr = native.alloc[Point] + ptr.x = x + ptr.y = y + ptr + } + } + + object LineSegment { + import implicits._ + def apply()(implicit z: native.Zone): native.Ptr[LineSegment] = + native.alloc[LineSegment] + def apply(a: native.Ptr[Point], b: native.Ptr[Point])( + implicit z: native.Zone): native.Ptr[LineSegment] = { + val ptr = native.alloc[LineSegment] + ptr.a = a + ptr.b = b + ptr + } + } + //#example +} +//#example diff --git a/docs/src/test/scala/org/example/Geometry.scala b/docs/src/test/scala/org/example/Geometry.scala new file mode 100644 index 0000000..4cbcc87 --- /dev/null +++ b/docs/src/test/scala/org/example/Geometry.scala @@ -0,0 +1,33 @@ +package org.example.geometry + +import scala.scalanative._ +import scala.scalanative.native._ + +@native.link("geometry") +@native.extern +object Geometry { + //#using-struct-point + type struct_circle = native.CStruct2[com.example.custom.binding.Vector.Point, native.CDouble] + //#using-struct-point + def circle_area(circle: native.Ptr[struct_circle]): native.CFloat = native.extern + + object implicits { + implicit class struct_circle_ops(val p: native.Ptr[struct_circle]) extends AnyVal { + def point: native.Ptr[com.example.custom.binding.Vector.Point] = p._1 + def point_=(value: native.Ptr[com.example.custom.binding.Vector.Point]): Unit = !p._1 = !value + def radius: native.CDouble = !p._2 + def radius_=(value: native.CDouble): Unit = !p._2 = value + } + } + + object struct_circle { + import implicits._ + def apply()(implicit z: native.Zone): native.Ptr[struct_circle] = native.alloc[struct_circle] + def apply(point: native.Ptr[com.example.custom.binding.Vector.Point], radius: native.CDouble)(implicit z: native.Zone): native.Ptr[struct_circle] = { + val ptr = native.alloc[struct_circle] + ptr.point = point + ptr.radius = radius + ptr + } + } +} diff --git a/docs/src/test/scala/org/example/WordCount.scala b/docs/src/test/scala/org/example/WordCount.scala new file mode 100644 index 0000000..35d4b62 --- /dev/null +++ b/docs/src/test/scala/org/example/WordCount.scala @@ -0,0 +1,36 @@ +package org.example.wordcount + +import scala.scalanative._ +import scala.scalanative.native._ + +@native.link("wordcount") +@native.extern +object WordCount { + type struct_wordcount = native.CStruct3[native.CLong, native.CLong, native.CLong] + //#using-stdio-file + def wordcount(wordcount: native.Ptr[struct_wordcount], file: native.Ptr[scala.scalanative.native.stdio.FILE]): native.CInt = native.extern + //#using-stdio-file + + object implicits { + implicit class struct_wordcount_ops(val p: native.Ptr[struct_wordcount]) extends AnyVal { + def chars: native.CLong = !p._1 + def chars_=(value: native.CLong): Unit = !p._1 = value + def lines: native.CLong = !p._2 + def lines_=(value: native.CLong): Unit = !p._2 = value + def words: native.CLong = !p._3 + def words_=(value: native.CLong): Unit = !p._3 = value + } + } + + object struct_wordcount { + import implicits._ + def apply()(implicit z: native.Zone): native.Ptr[struct_wordcount] = native.alloc[struct_wordcount] + def apply(chars: native.CLong, lines: native.CLong, words: native.CLong)(implicit z: native.Zone): native.Ptr[struct_wordcount] = { + val ptr = native.alloc[struct_wordcount] + ptr.chars = chars + ptr.lines = lines + ptr.words = words + ptr + } + } +} diff --git a/docs/src/test/scala/org/scalanative/bindgen/docs/GeometrySpec.scala b/docs/src/test/scala/org/scalanative/bindgen/docs/GeometrySpec.scala new file mode 100644 index 0000000..b1599e3 --- /dev/null +++ b/docs/src/test/scala/org/scalanative/bindgen/docs/GeometrySpec.scala @@ -0,0 +1,25 @@ +package org.scalanative.bindgen.docs + +import org.scalatest.FunSpec + +class GeometrySpec extends FunSpec { + describe("geometry") { + it("using generated bindings") { + //#example + import com.example.custom.binding.Vector.Point + import org.example.geometry.Geometry._ + import scala.scalanative.native.Zone + + Zone { implicit zone => + val center = Point(1, 1) + val circle = struct_circle(center, 2.0) + + val area = circle_area(circle) + //#example + assert(area - (4 * math.Pi) < 1e-3f) + //#example + } + //#example + } + } +} diff --git a/docs/src/test/scala/org/scalanative/bindgen/docs/VectorSpec.scala b/docs/src/test/scala/org/scalanative/bindgen/docs/VectorSpec.scala index 7616a61..8851fbf 100644 --- a/docs/src/test/scala/org/scalanative/bindgen/docs/VectorSpec.scala +++ b/docs/src/test/scala/org/scalanative/bindgen/docs/VectorSpec.scala @@ -25,12 +25,12 @@ class VectorSpec extends FunSpec { lineSegment2.b = struct_point(5, 0) //#step-2 //#step-3 - println(cosine(lineSegment1, lineSegment2)) + val angle = cosine(lineSegment1, lineSegment2) //#step-3 import org.scalactic.TolerantNumerics val epsilon = 1e-3f assert( - (0.8.toFloat === cosine(lineSegment1, lineSegment2))( + (0.8.toFloat === angle)( TolerantNumerics.tolerantFloatEquality(epsilon))) //#step-1 } diff --git a/docs/src/test/scala/org/scalanative/bindgen/docs/WordCountSpec.scala b/docs/src/test/scala/org/scalanative/bindgen/docs/WordCountSpec.scala new file mode 100644 index 0000000..dddb2dc --- /dev/null +++ b/docs/src/test/scala/org/scalanative/bindgen/docs/WordCountSpec.scala @@ -0,0 +1,37 @@ +package org.scalanative.bindgen.docs + +import org.scalatest.FunSpec + +class WordCountSpec extends FunSpec { + describe("wordcount") { + it("using generated bindings") { + //#example + import org.example.wordcount.WordCount._ + import scalanative.native._ + + //#example + val pathToFile = + c"docs/src/test/resources/scala-native-bindings/wordcount.h" + + import scalanative.posix.unistd.access + import scalanative.posix.fcntl.R_OK + assert(access(pathToFile, R_OK) == 0, "Header file does not exist") + + //#example + Zone { implicit zone => + val result = struct_wordcount() + val file = stdio.fopen(pathToFile, c"r") + val code = wordcount(result, file) + stdio.fclose(file) + //#example + import org.example.wordcount.WordCount.implicits._ + assert(code == 0) + assert(result.chars == 187) + assert(result.words == 20) + assert(result.lines == 11) + //#example + } + //#example + } + } +}