Leyendo archivo línea por línea en ensamblador – MASM32 – Parte 2

En el artículo anterior, vimos una forma efectiva para leer archivos de texto en ensamblador del MASM32 usando un único bloque de memoria o «buffer» de tamaño fijo. Este método es ahorrador de memoria, pero puede ser lento para archivos grandes, además de que se pone un límite al tamaño máximo de una línea.

En este artículo mostraré cómo se puede hacer una lectura línea por línea, pero haciendo un volcado completo del archivo, a memoria dinámica. Una vez en memoria, se irá explorando para encontrar el inicio y fin de cada línea.

Para empezar, partiremos de un código sencillo que nos permita hacer el trabajo de leer todo el contenido de un archivo a RAM:

include \masm32\include\masm32rt.inc

.data
    filePath db "input.tit",0
    hFile   dd ?     ;Manejador de archivo.
    f_size  dd ?     ;Tamaño del archivo a leer.
    buffer  dd ?     ;Puntero a la memoria dinámica

.code
start:
  ; Abrir el archivo para lectura
  invoke CreateFile, addr filePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
  mov hFile, eax ; Guardar el handle del archivo en la variable "hFile".

  ; Obtener el tamaño del archivo
  invoke GetFileSize, hFile, NULL
  mov f_size, eax ; Guardar el tamaño del archivo en la variable f_size

  ; Reservar memoria para el buffer que contendrá el contenido del archivo
  invoke GlobalAlloc, GMEM_FIXED, f_size
  mov buffer, eax ; Guardar el puntero al buffer en la variable "buffer".

  ; Leer el contenido del archivo en "buffer".
  invoke ReadFile, hFile, buffer, f_size, addr f_size, NULL

  ; Cerrar el archivo
  invoke CloseHandle, hFile

  ; Mostramos el contenido completo del archivo
  invoke MessageBox, NULL, buffer, NULL, NULL

  ; Liberar la memoria del buffer
  invoke GlobalFree, buffer

  invoke ExitProcess, 0
end start

Este código no lee línea por línea, sino que lee todo el contenido de un archivo en memoria y muestra también, todo ese contenido leído. Hay que tener cuidado si lo aplicamos a un archivo grande, porque «MessageBox» no será capaz de mostrar un texto muy grande.

Lo que haremos ahora, será modificar este código para que se adecúe a nuestro objetivo de poder leer línea por línea, lo que debe ser ya bastante fácil porque el archivo completo se encuentra en RAM. Solo necesitamos un medio para poder identificar el inicio y fin de las líneas. Para eso usaremos una técnica similar a la que usamos en el código anterior, que nos permite reconocer los delimitadores más comunes: CR, LF o CR/LF.

El código completo se muestra a continuación:

include \masm32\include\masm32rt.inc

.data
    filePath    db "input.tit",0
    hFile       dd ?    ;Manejador de archivo.
    bytesRead   dd ?    ;Tamaño del archivo leído.
    buffer      dd ?    ;Puntero a la memoria dinámica
    p_line      dd ?    ;Puntero a inicio de línea en "buffer".
    p_eol       dword ? ;Puntero a fin de línea en "buffer".
    c_eol       db ?    ;Caracter EOL encontrado. 
.code
;Proc. para abrir un archivo. Actualiza "hFile" y "f_eof".
open_file PROC  
    ; Abrir el archivo para lectura
    invoke CreateFile, addr filePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    mov hFile, eax ; Guardar el handle del archivo en la variable "hFile".
    ; Obtener el tamaño del archivo
    invoke GetFileSize, hFile, NULL
    inc eax         ; Pedimos un byte más para el delimitador.
    mov bytesRead, eax ; Guardar el tamaño del archivo en la variable bytesRead
    ; Reservar memoria para el buffer que contendrá el contenido del archivo
    invoke GlobalAlloc, GMEM_FIXED, bytesRead
    mov buffer, eax ; Guardar el puntero al buffer en la variable "buffer".
    ; Leer el contenido del archivo en "buffer".
    invoke ReadFile, hFile, buffer, bytesRead, addr bytesRead, NULL
    ;Escribe caracter nulo al final de la cadena leída
    mov edi, buffer
    add edi, bytesRead
    mov byte ptr [edi], 0
    ;Inicializamo puntero "p_line"
    mov edi, buffer
    mov p_line, edi ;Para que funcione bien read_eof().
    ;Preparamos para la primera lectura con search_EOL().
    dec edi             
    mov p_eol, edi  ;Deja apuntando al byte anterior.
    mov al, 0Ah    ;LF
    mov c_eol, al  ;Para que search_EOL() no intente buscar LF.
    ret
open_file ENDP
;Proc. para cerrar el archivo y liberar el espacio de memoria ocupado.
close_file PROC
    ; Cerrar el archivo
    invoke CloseHandle, hFile
    ; Liberar la memoria del buffer
    invoke GlobalFree, buffer
    ret
close_file ENDP
;Devuelve EAX=1 si se ha llegado al fin del archivo, de lo contrario 
;devuelve EAX=0.
read_eof PROC
    mov edi, buffer
    add edi, bytesRead
    .IF p_eol >= edi
        mov eax, 1
    .ELSE
        mov eax, 0
    .ENDIF
    ret
read_eof ENDP
;Pone delimitador \0 a línea apuntada por "p_line".
search_EOL PROC
    ;Validación.
    invoke read_eof
    .IF eax==1
        ret
    .ENDIF
    ;Prepara la lectura de la siguiente línea, a partir de "p_eol"
    mov esi, p_eol
    inc esi
    .IF c_eol == 0Dh    ;Se encontró el caracter CR
        .IF byte ptr [esi] == 0Ah   ;Sigue un LF. Es un CR-LF.
            inc esi ;Apuntamos a siguiente byte para pasar por alto el LF.
        .ENDIF
    .ENDIF
    mov p_line, esi     ;Aquí debería empezar la siguiente línea
    ;Posiciona "p_eol" al final de la línea.
    mov edi, buffer
    add edi, bytesRead  ; EDI <-buffer + bytesRead
    mov eax, p_line     
    mov p_eol, eax      ; p_eol <- p_line
    ;Validación.
    invoke read_eof
    .IF eax==1
        ret
    .ENDIF
    ;Busca delimitador
    .WHILE  p_eol<edi
        mov esi, p_eol
        .IF byte ptr [esi] == 0Ah ;¿Salto de línea LF?
            mov c_eol, 0Ah  ;Guarda caracter
            .BREAK 
        .ENDIF
        .IF byte ptr [esi] == 0Dh ;¿Salto de línea CR?
            mov c_eol, 0Dh  ;Guarda caracter
            .BREAK 
        .ENDIF
        inc p_eol     ;Siguiente byte
    .ENDW
    ;Marca fin de línea para que se muestre solo esa línea.
    mov esi, p_eol
    mov byte ptr [esi], 0h
    ret
search_EOL ENDP

    ;------------ Programa principal ---------------
start:
    invoke open_file
    invoke read_eof
    .WHILE eax==0 
        invoke search_EOL
        ; Mostramos el contenido completo del archivo
        invoke MessageBox, NULL, p_line, NULL, NULL

        invoke read_eof
    .ENDW

    invoke close_file
    invoke ExitProcess, 0
end start

La subrutina «open_file» hace el trabajo de mover todo el contenido del archivo a memoria dinámica en RAM, poniendo el delimitador de carácter nulo al final del bloque leído. También inicializa los punteros de línea y carácter, que son:

  •     p_line      -> Puntero al inicio de la línea en «buffer». Se va incrementando por cada línea leída.
  •     p_eol       -> Puntero al fin de línea en «buffer». Se va incrementando por cada línea leída.
  •     c_eol       -> Caracter EOL encontrado en la exploración anterior. Puede ser CR o LF.

Como ejemplo real, consideremos un archivo de texto con el contenido:

En un
lugar

El siguiente diagrama indica la posición de estas variables después de llamar a «open_file», con este archivo:

La subrutina «read_eof» verifica el estado del puntero «p_eol» para determinar cuando nos encontramos al final de una línea.

La subrutina «search_EOL» hace el trabajo complicado de actualizar los punteros «p_line» y «p_eol» de modo que siempre queden actualizados con la línea actual.

El siguiente diagrama muestra como se mueven los punteros por cada línea leída con «search_EOL»:

Si solo vamos a trabajar con un solo delimitador de tipo de línea como la clásica combinación CR/LF que se usa en archivos de Windows, podemos simplificar un poco el programa, en la siguiente forma:

include \masm32\include\masm32rt.inc

.data
    filePath    db "input.tit",0
    hFile       dd ?    ;Manejador de archivo.
    bytesRead   dd ?    ;Tamaño del archivo leído.
    buffer      dd ?    ;Puntero a la memoria dinámica
    p_line      dd ?    ;Puntero a inicio de línea en "buffer".
    p_eol       dword ? ;Puntero a fin de línea en "buffer".
.code
;Proc. para abrir un archivo. Actualiza "hFile" y "f_eof".
open_file PROC  
    ; Abrir el archivo para lectura
    invoke CreateFile, addr filePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    mov hFile, eax ; Guardar el handle del archivo en la variable "hFile".
    ; Obtener el tamaño del archivo
    invoke GetFileSize, hFile, NULL
    inc eax         ; Pedimos un byte más para el delimitador.
    mov bytesRead, eax ; Guardar el tamaño del archivo en la variable bytesRead
    ; Reservar memoria para el buffer que contendrá el contenido del archivo
    invoke GlobalAlloc, GMEM_FIXED, bytesRead
    mov buffer, eax ; Guardar el puntero al buffer en la variable "buffer".
    ; Leer el contenido del archivo en "buffer".
    invoke ReadFile, hFile, buffer, bytesRead, addr bytesRead, NULL
    ;Escribe caracter nulo al final de la cadena leída
    mov edi, buffer
    add edi, bytesRead
    mov byte ptr [edi], 0
    ;Inicializamos puntero "p_line"
    mov edi, buffer
    mov p_line, edi ;Para que funcione bien read_eof().
    ;Preparamos para la primera lectura con search_EOL().
    sub edi, 2
    mov p_eol, edi  ;Deja apuntando al penúltimo byte.
    ret
open_file ENDP
;Proc. para cerrar el archivo y liberar el espacio de memoria ocupado.
close_file PROC
    ; Cerrar el archivo
    invoke CloseHandle, hFile
    ; Liberar la memoria del buffer
    invoke GlobalFree, buffer
    ret
close_file ENDP
;Devuelve EAX=1 si se ha llegado al fin del archivo, de lo contrario 
;devuelve EAX=0.
read_eof PROC
    mov edi, buffer
    add edi, bytesRead
    .IF p_eol >= edi
        mov eax, 1
    .ELSE
        mov eax, 0
    .ENDIF
    ret
read_eof ENDP
;Pone delimitador \0 a línea apuntada por "p_line".
search_EOL PROC
    ;Validación.
    invoke read_eof
    .IF eax==1
        ret
    .ENDIF
    ;Prepara la lectura de la siguiente línea, a partir de "p_eol"
    mov esi, p_eol
    add esi, 2          ;Para saltar el CR/LF.
    mov p_line, esi     ;Aquí debería empezar la siguiente línea
    mov p_eol, esi      ; p_eol <- p_line
    ;Posiciona "p_eol" al final de la línea.
    mov edi, buffer
    add edi, bytesRead  ; EDI <-buffer + bytesRead
    ;Validación.
    invoke read_eof
    .IF eax==1
        ret
    .ENDIF
    ;Busca delimitador
    .WHILE  p_eol<edi
        mov esi, p_eol
        .IF byte ptr [esi] == 0Dh ;¿Salto de línea CR?
            .BREAK 
        .ENDIF
        inc p_eol     ;Siguiente byte
    .ENDW
    ;Marca fin de línea para que se muestre solo esa línea.
    mov esi, p_eol
    mov byte ptr [esi], 0h
    ret
search_EOL ENDP

    ;------------ Programa principal ---------------
start:
    invoke open_file
    invoke read_eof
    .WHILE eax==0 
        invoke search_EOL
        ; Mostramos el contenido completo del archivo
        invoke MessageBox, NULL, p_line, NULL, NULL

        invoke read_eof
    .ENDW

    invoke close_file
    invoke ExitProcess, 0
end start

Y con este código concluyo este artículo que se ha extendido más de lo que había planeado, pero puedo decir que se ha cumplido el objetivo.

De seguro que se puede mejorar el código. Las sugerencias son bienvenidas.

Para los que deseen, pueden ver los códigos en mi Github.


Sé el primero en comentar

Dejar una contestacion

Tu dirección de correo electrónico no será publicada.


*