I've a rough implementation here, but it needs testing (in particular, I'd like to ensure that there's no off-by-one errors with buffer lengths):
proc expandSymlink*(symlinkPath: string): string =
  ## Returns a string representing the path to which the symbolic link points.
  ##
  ## On Windows this is a noop, ``symlinkPath`` is simply returned.
  const bufsize = 32
  when defined(windows):
    var handle = openHandle(symlinkPath, false)
    when useWinUnicode:
      var
        buffer = newWideCString("", bufsize)
        length = GetFinalPathNameByHandleW(handle, buffer, len(buffer), 0)
      while length > len(buffer):
        setLen(buffer, length)
        length = GetFinalPathNameByHandleW(handle, buffer, len(buffer), 0)
    else:
      var
        result = newWideCString("", bufsize)
        length = GetFinalPathNameByHandleA(handle, buffer, len(buffer), 0)
      while length > len(buffer):
        setLen(buffer, length)
        length = GetFinalPathNameByHandleA(handle, buffer, len(buffer), 0)
    if length == 0:
      raiseOSError(osLastError())
    result = $buffer
  else:
    result = newString(bufsize)
    var length = readlink(symlinkPath, result, bufsize)
    while length > len(result):
      setLen(result, length)
      length = readlink(symlinkPath, result, length)
    if length < 0:
      raiseOSError(osLastError())
    setLen(result, length)