memfiles.nim 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2015 Nim Contributors
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## :Authors: Zahary Karadjov, Andreas Rumpf
  10. ##
  11. ## This module provides support for `memory mapped files`:idx:
  12. ## (Posix's `mmap`:idx:) on the different operating systems.
  13. ##
  14. ## It also provides some fast iterators over lines in text files (or
  15. ## other "line-like", variable length, delimited records).
  16. when defined(windows):
  17. import winlean
  18. elif defined(posix):
  19. import posix
  20. else:
  21. {.error: "the memfiles module is not supported on your operating system!".}
  22. import os, streams
  23. proc newEIO(msg: string): ref IOError =
  24. new(result)
  25. result.msg = msg
  26. type
  27. MemFile* = object ## represents a memory mapped file
  28. mem*: pointer ## a pointer to the memory mapped file. The pointer
  29. ## can be used directly to change the contents of the
  30. ## file, if it was opened with write access.
  31. size*: int ## size of the memory mapped file
  32. when defined(windows):
  33. fHandle*: Handle ## **Caution**: Windows specific public field to allow
  34. ## even more low level trickery.
  35. mapHandle*: Handle ## **Caution**: Windows specific public field.
  36. wasOpened*: bool ## **Caution**: Windows specific public field.
  37. else:
  38. handle*: cint ## **Caution**: Posix specific public field.
  39. flags: cint ## **Caution**: Platform specific private field.
  40. proc mapMem*(m: var MemFile, mode: FileMode = fmRead,
  41. mappedSize = -1, offset = 0, mapFlags = cint(-1)): pointer =
  42. ## returns a pointer to a mapped portion of MemFile `m`
  43. ##
  44. ## ``mappedSize`` of ``-1`` maps to the whole file, and
  45. ## ``offset`` must be multiples of the PAGE SIZE of your OS
  46. if mode == fmAppend:
  47. raise newEIO("The append mode is not supported.")
  48. var readonly = mode == fmRead
  49. when defined(windows):
  50. result = mapViewOfFileEx(
  51. m.mapHandle,
  52. if readonly: FILE_MAP_READ else: FILE_MAP_READ or FILE_MAP_WRITE,
  53. int32(offset shr 32),
  54. int32(offset and 0xffffffff),
  55. WinSizeT(if mappedSize == -1: 0 else: mappedSize),
  56. nil)
  57. if result == nil:
  58. raiseOSError(osLastError())
  59. else:
  60. assert mappedSize > 0
  61. m.flags = if mapFlags == cint(-1): MAP_SHARED else: mapFlags
  62. #Ensure exactly one of MAP_PRIVATE cr MAP_SHARED is set
  63. if int(m.flags and MAP_PRIVATE) == 0:
  64. m.flags = m.flags or MAP_SHARED
  65. result = mmap(
  66. nil,
  67. mappedSize,
  68. if readonly: PROT_READ else: PROT_READ or PROT_WRITE,
  69. m.flags,
  70. m.handle, offset)
  71. if result == cast[pointer](MAP_FAILED):
  72. raiseOSError(osLastError())
  73. proc unmapMem*(f: var MemFile, p: pointer, size: int) =
  74. ## unmaps the memory region ``(p, <p+size)`` of the mapped file `f`.
  75. ## All changes are written back to the file system, if `f` was opened
  76. ## with write access.
  77. ##
  78. ## ``size`` must be of exactly the size that was requested
  79. ## via ``mapMem``.
  80. when defined(windows):
  81. if unmapViewOfFile(p) == 0: raiseOSError(osLastError())
  82. else:
  83. if munmap(p, size) != 0: raiseOSError(osLastError())
  84. proc open*(filename: string, mode: FileMode = fmRead,
  85. mappedSize = -1, offset = 0, newFileSize = -1,
  86. allowRemap = false, mapFlags = cint(-1)): MemFile =
  87. ## opens a memory mapped file. If this fails, ``OSError`` is raised.
  88. ##
  89. ## ``newFileSize`` can only be set if the file does not exist and is opened
  90. ## with write access (e.g., with fmReadWrite).
  91. ##
  92. ##``mappedSize`` and ``offset``
  93. ## can be used to map only a slice of the file.
  94. ##
  95. ## ``offset`` must be multiples of the PAGE SIZE of your OS
  96. ## (usually 4K or 8K but is unique to your OS)
  97. ##
  98. ## ``allowRemap`` only needs to be true if you want to call ``mapMem`` on
  99. ## the resulting MemFile; else file handles are not kept open.
  100. ##
  101. ## ``mapFlags`` allows callers to override default choices for memory mapping
  102. ## flags with a bitwise mask of a variety of likely platform-specific flags
  103. ## which may be ignored or even cause `open` to fail if misspecified.
  104. ##
  105. ## Example:
  106. ##
  107. ## .. code-block:: nim
  108. ## var
  109. ## mm, mm_full, mm_half: MemFile
  110. ##
  111. ## mm = memfiles.open("/tmp/test.mmap", mode = fmWrite, newFileSize = 1024) # Create a new file
  112. ## mm.close()
  113. ##
  114. ## # Read the whole file, would fail if newFileSize was set
  115. ## mm_full = memfiles.open("/tmp/test.mmap", mode = fmReadWrite, mappedSize = -1)
  116. ##
  117. ## # Read the first 512 bytes
  118. ## mm_half = memfiles.open("/tmp/test.mmap", mode = fmReadWrite, mappedSize = 512)
  119. # The file can be resized only when write mode is used:
  120. if mode == fmAppend:
  121. raise newEIO("The append mode is not supported.")
  122. assert newFileSize == -1 or mode != fmRead
  123. var readonly = mode == fmRead
  124. template rollback =
  125. result.mem = nil
  126. result.size = 0
  127. when defined(windows):
  128. let desiredAccess = GENERIC_READ
  129. let shareMode = FILE_SHARE_READ
  130. let flags = FILE_FLAG_RANDOM_ACCESS
  131. template fail(errCode: OSErrorCode, msg: untyped) =
  132. rollback()
  133. if result.fHandle != 0: discard closeHandle(result.fHandle)
  134. if result.mapHandle != 0: discard closeHandle(result.mapHandle)
  135. raiseOSError(errCode)
  136. # return false
  137. #raise newException(IOError, msg)
  138. template callCreateFile(winApiProc, filename): untyped =
  139. winApiProc(
  140. filename,
  141. # GENERIC_ALL != (GENERIC_READ or GENERIC_WRITE)
  142. if readonly: desiredAccess else: desiredAccess or GENERIC_WRITE,
  143. if readonly: shareMode else: shareMode or FILE_SHARE_WRITE,
  144. nil,
  145. if newFileSize != -1: CREATE_ALWAYS else: OPEN_EXISTING,
  146. if readonly: FILE_ATTRIBUTE_READONLY or flags
  147. else: FILE_ATTRIBUTE_NORMAL or flags,
  148. 0)
  149. when useWinUnicode:
  150. result.fHandle = callCreateFile(createFileW, newWideCString(filename))
  151. else:
  152. result.fHandle = callCreateFile(createFileA, filename)
  153. if result.fHandle == INVALID_HANDLE_VALUE:
  154. fail(osLastError(), "error opening file")
  155. if newFileSize != -1:
  156. var
  157. sizeHigh = int32(newFileSize shr 32)
  158. sizeLow = int32(newFileSize and 0xffffffff)
  159. var status = setFilePointer(result.fHandle, sizeLow, addr(sizeHigh),
  160. FILE_BEGIN)
  161. let lastErr = osLastError()
  162. if (status == INVALID_SET_FILE_POINTER and lastErr.int32 != NO_ERROR) or
  163. (setEndOfFile(result.fHandle) == 0):
  164. fail(lastErr, "error setting file size")
  165. # since the strings are always 'nil', we simply always call
  166. # CreateFileMappingW which should be slightly faster anyway:
  167. result.mapHandle = createFileMappingW(
  168. result.fHandle, nil,
  169. if readonly: PAGE_READONLY else: PAGE_READWRITE,
  170. 0, 0, nil)
  171. if result.mapHandle == 0:
  172. fail(osLastError(), "error creating mapping")
  173. result.mem = mapViewOfFileEx(
  174. result.mapHandle,
  175. if readonly: FILE_MAP_READ else: FILE_MAP_READ or FILE_MAP_WRITE,
  176. int32(offset shr 32),
  177. int32(offset and 0xffffffff),
  178. if mappedSize == -1: 0 else: mappedSize,
  179. nil)
  180. if result.mem == nil:
  181. fail(osLastError(), "error mapping view")
  182. var hi, low: int32
  183. low = getFileSize(result.fHandle, addr(hi))
  184. if low == INVALID_FILE_SIZE:
  185. fail(osLastError(), "error getting file size")
  186. else:
  187. var fileSize = (int64(hi) shl 32) or int64(uint32(low))
  188. if mappedSize != -1: result.size = min(fileSize, mappedSize).int
  189. else: result.size = fileSize.int
  190. result.wasOpened = true
  191. if not allowRemap and result.fHandle != INVALID_HANDLE_VALUE:
  192. if closeHandle(result.fHandle) == 0:
  193. result.fHandle = INVALID_HANDLE_VALUE
  194. else:
  195. template fail(errCode: OSErrorCode, msg: string) =
  196. rollback()
  197. if result.handle != -1: discard close(result.handle)
  198. raiseOSError(errCode)
  199. var flags = if readonly: O_RDONLY else: O_RDWR
  200. if newFileSize != -1:
  201. flags = flags or O_CREAT or O_TRUNC
  202. var permissions_mode = S_IRUSR or S_IWUSR
  203. result.handle = open(filename, flags, permissions_mode)
  204. else:
  205. result.handle = open(filename, flags)
  206. if result.handle == -1:
  207. # XXX: errno is supposed to be set here
  208. # Is there an exception that wraps it?
  209. fail(osLastError(), "error opening file")
  210. if newFileSize != -1:
  211. if ftruncate(result.handle, newFileSize) == -1:
  212. fail(osLastError(), "error setting file size")
  213. if mappedSize != -1:
  214. result.size = mappedSize
  215. else:
  216. var stat: Stat
  217. if fstat(result.handle, stat) != -1:
  218. # XXX: Hmm, this could be unsafe
  219. # Why is mmap taking int anyway?
  220. result.size = int(stat.st_size)
  221. else:
  222. fail(osLastError(), "error getting file size")
  223. result.flags = if mapFlags == cint(-1): MAP_SHARED else: mapFlags
  224. #Ensure exactly one of MAP_PRIVATE cr MAP_SHARED is set
  225. if int(result.flags and MAP_PRIVATE) == 0:
  226. result.flags = result.flags or MAP_SHARED
  227. result.mem = mmap(
  228. nil,
  229. result.size,
  230. if readonly: PROT_READ else: PROT_READ or PROT_WRITE,
  231. result.flags,
  232. result.handle,
  233. offset)
  234. if result.mem == cast[pointer](MAP_FAILED):
  235. fail(osLastError(), "file mapping failed")
  236. if not allowRemap and result.handle != -1:
  237. if close(result.handle) == 0:
  238. result.handle = -1
  239. proc flush*(f: var MemFile; attempts: Natural = 3) =
  240. ## Flushes `f`'s buffer for the number of attempts equal to `attempts`.
  241. ## If were errors an exception `OSError` will be raised.
  242. var res = false
  243. var lastErr: OSErrorCode
  244. when defined(windows):
  245. for i in 1..attempts:
  246. res = flushViewOfFile(f.mem, 0) != 0
  247. if res:
  248. break
  249. lastErr = osLastError()
  250. if lastErr != ERROR_LOCK_VIOLATION.OSErrorCode:
  251. raiseOSError(lastErr)
  252. else:
  253. for i in 1..attempts:
  254. res = msync(f.mem, f.size, MS_SYNC or MS_INVALIDATE) == 0
  255. if res:
  256. break
  257. lastErr = osLastError()
  258. if lastErr != EBUSY.OSErrorCode:
  259. raiseOSError(lastErr, "error flushing mapping")
  260. when defined(posix) or defined(nimdoc):
  261. proc resize*(f: var MemFile, newFileSize: int) {.raises: [IOError, OSError].} =
  262. ## resize and re-map the file underlying an ``allowRemap MemFile``.
  263. ## **Note**: this assumes the entire file is mapped read-write at offset zero.
  264. ## Also, the value of ``.mem`` will probably change.
  265. ## **Note**: This is not (yet) available on Windows.
  266. when defined(posix):
  267. if f.handle == -1:
  268. raise newException(IOError,
  269. "Cannot resize MemFile opened with allowRemap=false")
  270. if ftruncate(f.handle, newFileSize) == -1:
  271. raiseOSError(osLastError())
  272. when defined(linux): #Maybe NetBSD, too?
  273. #On Linux this can be over 100 times faster than a munmap,mmap cycle.
  274. proc mremap(old: pointer; oldSize, newSize: csize; flags: cint):
  275. pointer {.importc: "mremap", header: "<sys/mman.h>".}
  276. let newAddr = mremap(f.mem, csize(f.size), csize(newFileSize), cint(1))
  277. if newAddr == cast[pointer](MAP_FAILED):
  278. raiseOSError(osLastError())
  279. else:
  280. if munmap(f.mem, f.size) != 0:
  281. raiseOSError(osLastError())
  282. let newAddr = mmap(nil, newFileSize, PROT_READ or PROT_WRITE,
  283. f.flags, f.handle, 0)
  284. if newAddr == cast[pointer](MAP_FAILED):
  285. raiseOSError(osLastError())
  286. f.mem = newAddr
  287. f.size = newFileSize
  288. proc close*(f: var MemFile) =
  289. ## closes the memory mapped file `f`. All changes are written back to the
  290. ## file system, if `f` was opened with write access.
  291. var error = false
  292. var lastErr: OSErrorCode
  293. when defined(windows):
  294. if f.wasOpened:
  295. error = unmapViewOfFile(f.mem) == 0
  296. if not error:
  297. error = closeHandle(f.mapHandle) == 0
  298. if not error and f.fHandle != INVALID_HANDLE_VALUE:
  299. discard closeHandle(f.fHandle)
  300. f.fHandle = INVALID_HANDLE_VALUE
  301. if error:
  302. lastErr = osLastError()
  303. else:
  304. error = munmap(f.mem, f.size) != 0
  305. lastErr = osLastError()
  306. if f.handle != -1:
  307. error = (close(f.handle) != 0) or error
  308. f.size = 0
  309. f.mem = nil
  310. when defined(windows):
  311. f.fHandle = 0
  312. f.mapHandle = 0
  313. f.wasOpened = false
  314. else:
  315. f.handle = -1
  316. if error: raiseOSError(lastErr)
  317. type MemSlice* = object ## represent slice of a MemFile for iteration over delimited lines/records
  318. data*: pointer
  319. size*: int
  320. proc `==`*(x, y: MemSlice): bool =
  321. ## Compare a pair of MemSlice for strict equality.
  322. result = (x.size == y.size and equalMem(x.data, y.data, x.size))
  323. proc `$`*(ms: MemSlice): string {.inline.} =
  324. ## Return a Nim string built from a MemSlice.
  325. result.setLen(ms.size)
  326. copyMem(addr(result[0]), ms.data, ms.size)
  327. iterator memSlices*(mfile: MemFile, delim = '\l', eat = '\r'): MemSlice {.inline.} =
  328. ## Iterates over [optional `eat`] `delim`-delimited slices in MemFile `mfile`.
  329. ##
  330. ## Default parameters parse lines ending in either Unix(\\l) or Windows(\\r\\l)
  331. ## style on on a line-by-line basis. I.e., not every line needs the same ending.
  332. ## Unlike readLine(File) & lines(File), archaic MacOS9 \\r-delimited lines
  333. ## are not supported as a third option for each line. Such archaic MacOS9
  334. ## files can be handled by passing delim='\\r', eat='\\0', though.
  335. ##
  336. ## Delimiters are not part of the returned slice. A final, unterminated line
  337. ## or record is returned just like any other.
  338. ##
  339. ## Non-default delimiters can be passed to allow iteration over other sorts
  340. ## of "line-like" variable length records. Pass eat='\\0' to be strictly
  341. ## `delim`-delimited. (Eating an optional prefix equal to '\\0' is not
  342. ## supported.)
  343. ##
  344. ## This zero copy, memchr-limited interface is probably the fastest way to
  345. ## iterate over line-like records in a file. However, returned (data,size)
  346. ## objects are not Nim strings, bounds checked Nim arrays, or even terminated
  347. ## C strings. So, care is required to access the data (e.g., think C mem*
  348. ## functions, not str* functions).
  349. ##
  350. ## Example:
  351. ##
  352. ## .. code-block:: nim
  353. ## var count = 0
  354. ## for slice in memSlices(memfiles.open("foo")):
  355. ## if slice.size > 0 and cast[cstring](slice.data)[0] != '#':
  356. ## inc(count)
  357. ## echo count
  358. proc c_memchr(cstr: pointer, c: char, n: csize): pointer {.
  359. importc: "memchr", header: "<string.h>".}
  360. proc `-!`(p, q: pointer): int {.inline.} = return cast[int](p) -% cast[int](q)
  361. var ms: MemSlice
  362. var ending: pointer
  363. ms.data = mfile.mem
  364. var remaining = mfile.size
  365. while remaining > 0:
  366. ending = c_memchr(ms.data, delim, remaining)
  367. if ending == nil: # unterminated final slice
  368. ms.size = remaining # Weird case..check eat?
  369. yield ms
  370. break
  371. ms.size = ending -! ms.data # delim is NOT included
  372. if eat != '\0' and ms.size > 0 and cast[cstring](ms.data)[ms.size - 1] == eat:
  373. dec(ms.size) # trim pre-delim char
  374. yield ms
  375. ms.data = cast[pointer](cast[int](ending) +% 1) # skip delim
  376. remaining = mfile.size - (ms.data -! mfile.mem)
  377. iterator lines*(mfile: MemFile, buf: var TaintedString, delim = '\l',
  378. eat = '\r'): TaintedString {.inline.} =
  379. ## Replace contents of passed buffer with each new line, like
  380. ## `readLine(File) <io.html#readLine,File,TaintedString>`_.
  381. ## `delim`, `eat`, and delimiting logic is exactly as for `memSlices
  382. ## <#memSlices.i,MemFile,char,char>`_, but Nim strings are returned.
  383. ##
  384. ## Example:
  385. ##
  386. ## .. code-block:: nim
  387. ## var buffer: TaintedString = ""
  388. ## for line in lines(memfiles.open("foo"), buffer):
  389. ## echo line
  390. for ms in memSlices(mfile, delim, eat):
  391. setLen(buf.string, ms.size)
  392. if ms.size > 0:
  393. copyMem(addr buf[0], ms.data, ms.size)
  394. yield buf
  395. iterator lines*(mfile: MemFile, delim = '\l', eat = '\r'): TaintedString {.inline.} =
  396. ## Return each line in a file as a Nim string, like
  397. ## `lines(File) <io.html#lines.i,File>`_.
  398. ## `delim`, `eat`, and delimiting logic is exactly as for `memSlices
  399. ## <#memSlices.i,MemFile,char,char>`_, but Nim strings are returned.
  400. ##
  401. ## Example:
  402. ##
  403. ## .. code-block:: nim
  404. ## for line in lines(memfiles.open("foo")):
  405. ## echo line
  406. var buf = TaintedString(newStringOfCap(80))
  407. for line in lines(mfile, buf, delim, eat):
  408. yield buf
  409. type
  410. MemMapFileStream* = ref MemMapFileStreamObj ## a stream that encapsulates a `MemFile`
  411. MemMapFileStreamObj* = object of Stream
  412. mf: MemFile
  413. mode: FileMode
  414. pos: ByteAddress
  415. proc mmsClose(s: Stream) =
  416. MemMapFileStream(s).pos = -1
  417. close(MemMapFileStream(s).mf)
  418. proc mmsFlush(s: Stream) = flush(MemMapFileStream(s).mf)
  419. proc mmsAtEnd(s: Stream): bool = (MemMapFileStream(s).pos >= MemMapFileStream(s).mf.size) or
  420. (MemMapFileStream(s).pos < 0)
  421. proc mmsSetPosition(s: Stream, pos: int) =
  422. if pos > MemMapFileStream(s).mf.size or pos < 0:
  423. raise newEIO("cannot set pos in stream")
  424. MemMapFileStream(s).pos = pos
  425. proc mmsGetPosition(s: Stream): int = MemMapFileStream(s).pos
  426. proc mmsPeekData(s: Stream, buffer: pointer, bufLen: int): int =
  427. let startAddress = cast[ByteAddress](MemMapFileStream(s).mf.mem)
  428. let p = cast[ByteAddress](MemMapFileStream(s).pos)
  429. let l = min(bufLen, MemMapFileStream(s).mf.size - p)
  430. moveMem(buffer, cast[pointer](startAddress + p), l)
  431. result = l
  432. proc mmsReadData(s: Stream, buffer: pointer, bufLen: int): int =
  433. result = mmsPeekData(s, buffer, bufLen)
  434. inc(MemMapFileStream(s).pos, result)
  435. proc mmsWriteData(s: Stream, buffer: pointer, bufLen: int) =
  436. if MemMapFileStream(s).mode == fmRead:
  437. raise newEIO("cannot write to read-only stream")
  438. let size = MemMapFileStream(s).mf.size
  439. if MemMapFileStream(s).pos + bufLen > size:
  440. raise newEIO("cannot write to stream")
  441. let p = cast[ByteAddress](MemMapFileStream(s).mf.mem) +
  442. cast[ByteAddress](MemMapFileStream(s).pos)
  443. moveMem(cast[pointer](p), buffer, bufLen)
  444. inc(MemMapFileStream(s).pos, bufLen)
  445. proc newMemMapFileStream*(filename: string, mode: FileMode = fmRead,
  446. fileSize: int = -1): MemMapFileStream =
  447. ## creates a new stream from the file named `filename` with the mode `mode`.
  448. ## Raises ## `OSError` if the file cannot be opened. See the `system
  449. ## <system.html>`_ module for a list of available FileMode enums.
  450. ## ``fileSize`` can only be set if the file does not exist and is opened
  451. ## with write access (e.g., with fmReadWrite).
  452. var mf: MemFile = open(filename, mode, newFileSize = fileSize)
  453. new(result)
  454. result.mode = mode
  455. result.mf = mf
  456. result.closeImpl = mmsClose
  457. result.atEndImpl = mmsAtEnd
  458. result.setPositionImpl = mmsSetPosition
  459. result.getPositionImpl = mmsGetPosition
  460. result.readDataImpl = mmsReadData
  461. result.peekDataImpl = mmsPeekData
  462. result.writeDataImpl = mmsWriteData
  463. result.flushImpl = mmsFlush