stack_strings

Search:
Group by:
Source   Edit  

The stack_strings module provides a string implementation that works with 100% stack memory.

This module is primarily meant for programs that want to avoid any and all heap allocation, such as code for embedded targets. If you use --mm:arc and -d:useMalloc in tandem with this module, your program will be able to do string operations without allocating any memory at runtime.

The StackString Type

The StackString type is an object with a fixed size buffer and an integer to store its current length. It works very similarly to string, but its internal buffer cannot be resized, and must be known at compile time.

Note that like Nim and C strings, StackString is nil-terminated, so its internal buffer size will be your specified size + 1.

To create an empty StackString, use the stackStringOfCap proc:

Example:

import stack_strings
var emptyStr = stackStringOfCap(10)

Note the lack of new in the name; there is no runtime allocation going on here.

Under the hood, a StackString[10] object was created and returned, and its length was set to 0. Since buffers are fixed-size and known at compile time, the capacity of the StackString is encoded as part of its type.

You can add to a StackString, assuming it has capacity:

Example:

import stack_strings
var name = "John"

var greeting = stackStringOfCap(32)

greeting.add("Hello, ")
greeting.add(name)
greeting.add('!')

See: add, tryAdd, addTruncate and unsafeAdd.

Since using stackStringOfCap for creating StackString constants is both annoying and inefficient, there exists a more convenient way of creating StackString objects.

If you have a static string (such as a string literal), you can use the ss proc to create a StackString automatically:

Example:

import stack_strings
let greeting = ss"Hello, world!"

doAssert greeting is StackString[13]

The resulting StackString's capacity will be the length of the static string provided. In the case of the code above, the type of greeting is StackString[13].

If you have a runtime string (or anything else that's covered by the IndexableChars union type) that you want to convert to a StackString, you can use toStackString:

Example:

import stack_strings
let someNimString = "The size of this will not be known"

# A capacity of 64 is more than enough
var someSs1 = someNimString.toStackString(64)
doAssert someSs1 == ss"The size of this will not be known"
See also: unsafeToStackString, tryToStackString, toStackStringTruncate.

Manipulating StackString objects

In Nim, string is mutable if it is stored in a var, as opposed to a let. The same applies to StackString.

You can manipulate a StackString by setting individual chars using the same syntax as for string:

Example:

import stack_strings
var str = ss"hello"

str[0] = 'y'

doAssert str == "yello"

You can also set length by calling setLen. The difference between StackString's setLen versus string's setLen is that StackString's cannot resize the internal buffer. For this reason, resizing a StackString beyond its capacity will result in an error.

Similarly, you can use add to append to a StackString, but you cannot append beyond its capacity, otherwise you will encounter a runtime error.

Preventing Accidental Allocations

Due to the implicit nature of Nim, allocating memory is extremely easy to do on accident.

This module provides a few optional compiler flags you can enable to make allocation from this the module's procs impossible:

Read the documentation for any of these flags to learn more about them.

The easiest way to open yourself up to surprise allocation (and arguably surprise behavior in general) is triggering exceptions. Nim exceptions are always heap-allocated, so to avoid the possibility of runtime memory allocation, you must eliminate exceptions.

Most access and mutation procs have try- and unsafe- prefixed variants which do not raise any exceptions. For example, `[]` has tryGet/unsafeGet, `[]=` has trySet/unsafeSet and setLen has trySetLen/unsafeSetLen.

The flags discussed previously can help you enforce use of these non-exception variants.

Using StackString as an openArray

If you want to use a StackString object as openArray[char], you can use the toOpenArray template. This template does not copy any memory.

Using StackString with the unicode Module

The stdlib's unicode module supports openArray[char], so all you have to do to use StackString with it is to use the toOpenArray template first.

Types

IndexableChars = cstring | string | openArray[char] | StackString
Indexable data types that contain chars Source   Edit  
InsufficientCapacityDefect = object of Defect
  capacity*: Natural         ## The [StackString]'s capacity
  requestedCapacity*: Natural ## The capacity required to successfully complete the operation
  
Defect raised when attempting to append to or change the length of a StackString that does not have enough capacity to accomodate the new length Source   Edit  
StackString[Size] = object
  ## The current string length
  data*: array[Size + 1, char] ## The underlying string data.
                               ## If you just want to iterate over the string's characters, use the [items] iterator.
  
A stack-allocated string with a fixed capacity Source   Edit  

Consts

fatalOnStackStringDollar {.booldefine.} = false
When true, shows a compiler fatal error when a StackString is converted to a string via the $ proc Source   Edit  
stackStringsPreventAllocation {.booldefine.} = false
When true, prevents any heap allocations from occuring in this module by showing compiler errors on operations that can allocate memory at runtime Source   Edit  
warnOnStackStringDollar {.booldefine.} = false
When true, shows a compiler warning when a StackString is converted to a string via the $ proc Source   Edit  

Procs

func `$`(this: StackString): string {.inline.}

Converts the StackString to a string. Note that this proc allocates a new string and copies the contents of the StackString into the newly created string.

See warnOnStackStringDollar and fatalOnStackStringDollar for information about compiler warnings errors this may cause. If you want to avoid any warnings or errors specific to this proc, use toString instead (which is intentionally more explicit).

Source   Edit  
func `==`(this: StackString; str: IndexableChars): bool {.inline.}
Returns whether the StackString's content is equal to the content of another set of characters

Example:

let str1 = ss"abc"
let str2 = ss"abc"
let str3 = ss"nope"

let heapStr1 = "abc"
let heapStr2 = "nope"

doAssert str1 == str2
doAssert str1 != str3
doAssert str1 == heapStr1
doAssert str2 != heapStr2
Source   Edit  
func `[]`(this: StackString; i: Natural | BackwardsIndex): char {.inline,
    ...raises: [IndexDefect].}
Returns the character at the specified index in the StackString, or raises IndexDefect if the index is invalid

Example:

let str = ss"Hello world"

doAssert str[^1] == 'd'
Source   Edit  
func `[]=`(this: var StackString; i: Natural | BackwardsIndex; value: char) {.
    inline, ...raises: [IndexDefect].}
Sets the character at the specified index in the StackString, or raises IndexDefect if the index is invalid

Example:

var str = ss"Hello world"

str[0] = 'Y'
doAssert str == "Yello world"
Source   Edit  
proc add(this: var StackString; strOrChar: auto) {.inline,
    ...raises: [InsufficientCapacityDefect].}

Appends the provided value to the StackString. If there is not enough capacity to accomodate the new value, InsufficientCapacityDefect will be raised.

If you don't want to deal with exceptions, you can use tryAdd or addTruncate instead.

Example:

var bigCap = stackStringOfCap(10)
var smallCap = stackStringOfCap(3)

let strToAdd = ss"Hello"

bigCap.add(strToAdd)
doAssertRaises InsufficientCapacityDefect, smallCap.add(strToAdd)

doAssert bigCap == "Hello"
doAssert smallCap == ""

bigCap.add('!')
doAssert bigCap == "Hello!"
Source   Edit  
proc addTruncate(this: var StackString; strOrChar: auto): bool {.inline,
    discardable.}

Appends the provided value to the StackString. If the capacity of the StackString is not enough to accomodate the value, the chars that cannot be appended will be truncated. If the provided value is truncated, false will be returned. Otherwise, true will be returned.

If you want to use a version that raises an exception when there is not enough, you can use add instead. If you want to avoid raising an exception when there is not enough capacity, you can use tryAdd instead.

Example:

var bigCap = stackStringOfCap(10)
var smallCap = stackStringOfCap(3)

let strToAdd = ss"Hello"

doAssert bigCap.addTruncate(strToAdd) == true
doAssert smallCap.addTruncate(strToAdd) == false

doAssert bigCap == "Hello"
doAssert smallCap == "Hel"
Source   Edit  
func capacity(this: StackString): Natural {.inline.}
Returns the capacity of the StackString

Example:

var ssLit = ss"Same capacity"

doAssert ssLit.capacity == 13

var extraCap = stackStringOfCap(10)
extraCap.add("Hi")

doAssert extraCap.capacity == 10
doAssert extraCap.len == 2
Source   Edit  
func contains(this: StackString; c: char): bool {.inline.}
Returns whether the specified char can be found within the StackString

Example:

let str = ss"abcdef"

doAssert str.contains('c') == true
doAssert str.contains('f') == true
doAssert str.contains('z') == false
Source   Edit  
func contains(this: StackString; substr: StackString | string | IndexableChars): bool {.
    inline.}
Returns whether the specified substring can be found within the StackString

Example:

let str = ss"abcdef"

doAssert str.contains("cde") == true
doAssert str.contains("def") == true
doAssert str.contains("f") == true
doAssert str.contains("abd") == false
doAssert str.contains("zef") == false
Source   Edit  
func find(this: StackString; c: char): int {.inline.}
Finds the index of the specified char in the StackString, or returns -1 if the char was not found

Example:

let str = ss"abcdef"

doAssert str.find('c') == 2
doAssert str.find('f') == 5
doAssert str.find('z') == -1
Source   Edit  
func find(this: StackString; substr: StackString | string | IndexableChars): int {.
    inline.}
Finds the index of the specified substring in the StackString, or returns -1 if the substring was not found

Example:

let str = ss"abcdef"

doAssert str.find("cde") == 2
doAssert str.find("def") == 3
doAssert str.find("f") == 5
doAssert str.find("abd") == -1
doAssert str.find("zef") == -1
Source   Edit  
func high(this: StackString): int {.inline.}
Returns the highest index of the StackString, or -1 if it is empty

Example:

var str1 = "Hello world"
var str2 = ""

doAssert str1.high == 10
doAssert str2.high == -1
Source   Edit  
func len(this: StackString): Natural {.inline.}
The current string length Source   Edit  
proc newInsufficientCapacityDefect(msg: string; capacity: Natural;
                                   requestedCapacity: Natural): ref InsufficientCapacityDefect {.
    ...raises: [], tags: [], forbids: [].}
Allocates a new InsufficientCapacityDefect relating to the specified StackString Source   Edit  
proc setLen(this: var StackString; newLen: Natural | BackwardsIndex;
            writeZerosOnTruncate: bool = true) {.inline,
    ...raises: [InsufficientCapacityDefect].}

Sets the length of the StackString to newLen. If newLen is more than the StackString's capacity, InsufficientCapacityDefect will be raised.

If writeZerosOnTruncate is true and newLen is less than the current capacity, the truncated bytes will be zeroed out.

Example:

var str1 = ss"Hello world"
var str2 = ss"Hi world"
var str3 = ss"abc"

# Note that all [StackString] objects' data are nil-terminated

# The string will be truncated, and the truncated data will be overwritten with zeros
str1.setLen(5)
doAssert str1 == "Hello"
doAssert str1.data == ['H', 'e', 'l', 'l', 'o', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00']

# If we're sure it's safe to skip overwriting the truncated data with zeros, we can disable it
str2.setLen(2, writeZerosOnTruncate = false)
doAssert str2 == "Hi"
doAssert str2.data == ['H', 'i', ' ', 'w', 'o', 'r', 'l', 'd', '\x00']

# It works with BackwardsIndex, too
str3.setLen(^1)
doAssert str3 == "ab"
Source   Edit  
func ss(str: static string): static auto
Creates a StackString object from a static string. The StackString's capacity will be the string's actual length.

Example:

let name = ss"John Doe"

doAssert name is StackString[8]
Source   Edit  
func stackStringOfCap(capacity: static Natural): static auto
Creates a StackString with the specified capacity. This proc does not allocate heap memory.

Example:

var str = stackStringOfCap(10)

doAssert str is StackString[10]
doAssert str.len == 0
Source   Edit  
proc toHeapCstring(this: StackString): cstring {.inline.}
Allocates a cstring on the heap and copies the contents of the StackString into it. The cstring is a pointer to heap memory which must be freed manually by the caller using dealloc. If you just want to get the StackString's data stack pointer as a cstring, use toCstring instead.

Example:

let str = ss"Hello world"
let cstr = str.toHeapCstring()

doAssert cstr == "Hello world"
when NimMajor > 1: ## Nim 2.0 no longer requires `unsafeaddr`
    doAssert cstr[0].addr != addr str.data[0]
else:
    doAssert cstr[0].unsafeaddr != unsafeaddr str.data[0]

# You need to deallocate the cstring when you're done with it
dealloc(cstr)
Source   Edit  
proc toStackString(content: IndexableChars; size: static Natural): StackString[
    size] {.inline.}
Creates a new StackString of the specified size using the provided content. If you don't want to raise a defect when the input string exceeds the specified size, use tryToStackString. If you want to truncate the content in the resulting StackString if it's too long, use toStackStringTruncate.

Example:

let nimStr = "hi"
var stackStr = nimStr.toStackString(10)

doAssert stackStr.len == 2

stackStr.add(" world")

doAssert stackStr.len == 8

doAssertRaises InsufficientCapacityDefect, stackStr.add(", and everyone in it!")
Source   Edit  
proc toStackStringTruncate(content: IndexableChars; size: static Natural): StackString[
    size] {.inline.}
Creates a new StackString of the specified size using the provided content. If the content length is more than size, only the part of the content that can fit in the size will be included, and the rest will be truncated.

Example:

let nimStr = "Hello world"
let stackStr = nimStr.toStackStringTruncate(5)

doAssert stackStr == "Hello"
Source   Edit  
func toString(this: StackString): string

Allocates a new string with the content of the provided StackString. Note that this will allocate heap memory and copy the StackString's content.

This proc won't generate any compiler warnings or errors, unlike `$`, which has the possibility of doing so. See documentation for StackString's `$` proc for more info.

Source   Edit  
proc tryAdd(this: var StackString; strOrChar: auto): bool {.inline.}

Appends the value to the StackString. If there is enough capacity to accomodate the new value, true will be returned. If there is not enough capacity to accomodate the new value, false will be returned.

If you want to use a version that raises an exception, you can use add instead. If you want to append as much as possible and then truncate whatever doesn't fit, you can use addTruncate instead.

Example:

var bigCap = stackStringOfCap(10)
var smallCap = stackStringOfCap(3)

let strToAdd = ss"Hello"

doAssert bigCap.tryAdd(strToAdd) == true
doAssert smallCap.tryAdd(strToAdd) == false

doAssert bigCap == "Hello"
doAssert smallCap == ""
Source   Edit  
func tryGet(this: StackString; i: Natural | BackwardsIndex): Option[char]
Returns the character at the specified index in the StackString, or returns None if the index is invalid

Example:

import std/options

let str = ss"Hello world"

let char1 = str.tryGet(0)
let char2 = str.tryGet(^1)
let char3 = str.tryGet(100)

doAssert char1.isSome
doAssert char2.isSome
doAssert char3.isNone
Source   Edit  
func trySet(this: var StackString; i: Natural | BackwardsIndex; value: char): bool
Sets the character at the specified index in the StackString and returns true, or returns false if the index is invalid Source   Edit  
proc trySetLen(this: var StackString; newLen: Natural | BackwardsIndex;
               writeZerosOnTruncate: bool = true): bool {.inline.}

Sets the length of the StackString to newLen, then returns true. If newLen is more than the StackString's capacity, false will be returned.

If writeZerosOnTruncate is true and newLen is less than the current capacity, the truncated bytes will be zeroed out.

Example:

var str1 = ss"Hello world"

doAssert str1.trySetLen(5) == true
doAssert str1.trySetLen(11) == true
doAssert str1.trySetLen(12) == false
Source   Edit  
proc tryToStackString(content: IndexableChars; size: static Natural): Option[
    StackString[size]] {.inline.}
Creates a new StackString of the specified size using the provided content. If the content's length is more than the size argument, then None will be returned. If you want to raise a defect when the input string exceeds the specified size, use toStackString. If you want to truncate the content in the resulting StackString if it's too long, use toStackStringTruncate.

Example:

import std/options

let nimStr = "too long very long"
let stackStrRes = nimStr.tryToStackString(10)

doAssert stackStrRes.isNone

let stackStrRes2 = nimStr.tryToStackString(32)

doAssert stackStrRes2.isSome
Source   Edit  
proc unsafeAdd(this: var StackString; strOrChar: auto) {.inline.}
Appends the value to the StackString. No capacity checks are performed whatsoever; only use this when you are 100% sure there is enough capacity!

Example:

var bigCap = stackStringOfCap(10)

let strToAdd = ss"Hello"

bigCap.unsafeAdd(strToAdd)
Source   Edit  
func unsafeGet(this: StackString; i: Natural | BackwardsIndex): char {.inline.}

Returns the character at the specified index in the StackString.

Performs no bounds checks whatsoever; use only if you're 100% sure your index won't extend beyond the StackString's capacity + its nil terminator. Since no checks are performed, you can read past the StackString's length, but reading past its capacity + its nil terminator is undefined behavior and may crash. In most cases, you'll be reading zeros past the StackString's length, unless you used setLen with writeZerosOnTruncate set to false.

Source   Edit  
func unsafeSet(this: var StackString; i: Natural | BackwardsIndex; value: char) {.
    inline.}

Sets the character at the specified index in the StackString.

Performs no bounds checks whatsoever; use only if you're 100% sure your index won't extend beyond the StackString's capacity. Since no checks are performed, you can write past the StackString's length, but writing past its capacity is undefined behavior and may crash. Overwriting the StackString's nil terminator will make the string unusable when interopping with C.

Source   Edit  
proc unsafeSetLen(this: var StackString; newLen: Natural | BackwardsIndex;
                  writeZerosOnTruncate: bool = true) {.inline.}

Sets the length of the StackString to newLen. No capacity checks are performed whatsoever; only use this if you're 100% sure you are not exceeding capacity!

If writeZerosOnTruncate is true and newLen is less than the current capacity, the truncated bytes will be zeroed out.

Source   Edit  
proc unsafeToStackString(content: IndexableChars; size: static Natural): StackString[
    size] {.inline.}
Creates a new StackString of the specified size using the provided content. No capacity checks are performed whatsoever; only use this when you are 100% sure that the content's length is less than or equal to the specified size!

Example:

let nimStr = "Some runtime string"

const maxSize = 32

# We're already doing a capacity check, so we can use the unsafe version to avoid a redundant check
if nimStr.len > maxSize:
    echo "String is too long!"
else:
    let stackStr = nimStr.unsafeToStackString(maxSize)

    doAssert stackStr.len == nimStr.len
    
Source   Edit  

Iterators

iterator items(this: StackString): char {.inline.}
Iterates over each char in the StackString

Example:

let str = ss"abc"

var chars = newSeq[char]()

for c in str.items:
    chars.add(c)

doAssert chars == @['a', 'b', 'c']
Source   Edit  
iterator mitems(this: var StackString): var char {.inline.}
Iterates over each char in the StackString, returning a mutable reference

Example:

var str = ss"fly in the sky"

for c in str.mitems:
    if c == 'f':
        c = 's'
    elif c == 'k':
        c = 't'

doAssert str == "sly in the sty"
Source   Edit  
iterator mpairs(this: var StackString): (int, var char) {.inline.}
Iterates over each index-char pairs in the StackString, returning a mutable reference to the char

Example:

var str = ss"ooo"

for (i, c) in str.mpairs:
    if i == 0 or i == 2:
        c = 'l'

doAssert str == "lol"
Source   Edit  
iterator pairs(this: StackString): (int, char) {.inline.}
Iterates over each index-char pairs in the StackString

Example:

let str = ss"abc"

var strPairs = newSeq[(int, char)]()

for pair in str.pairs:
    strPairs.add(pair)

doAssert strPairs[0] == (0, 'a')
doAssert strPairs[^1] == (2, 'c')
Source   Edit  

Templates

template `[]`(this: StackString; slice: HSlice): openArray[char]

Returns an openArray for the specified range in the StackString, or raises RangeDefect if the range is invalid. The returned range is a reference to the original StackString data memory.

If you want to use the openArray value as an actual value that can be stored in a variable, you need to enable the experimental views feature.

Example:

{.experimental: "views".}

let str = ss"Hello world"

let part = str[0..4]

doAssert part == ['H', 'e', 'l', 'l', 'o']
when NimMajor > 1: ## Nim 2.0 no longer requires `unsafeaddr`
    doAssert part[0].addr == addr str.data[0]
else:
    doAssert part[0].unsafeaddr == unsafeaddr str.data[0]
Source   Edit  
template toCstring(this: StackString): cstring

Converts the StackString to cstring. Note that no memory copying is done; this simply casts the StackString's data to cstring.

There are a few implications to this:

  • The reference will only live as long as its scope; use toHeapCstring if you want a heap-allocated cstring
  • If you used setLen with writeZerosOnTruncate set to false, the content of the cstring will include the truncated part of the string

Example:

let str = ss"Hello world"
let cstr = str.toCstring()

doAssert cstr == "Hello world"
when NimMajor > 1: ## Nim 2.0 no longer requires `unsafeaddr` 
    doAssert cstr[0].addr == addr str.data[0]
else:
    doAssert cstr[0].unsafeaddr == unsafeaddr str.data[0]
Source   Edit  
template toOpenArray(this: StackString): untyped
Converts the StackString to openArray[char]. Thanks to ElegantBeef for help on this template. Source   Edit