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.
¿Cómo citar este artículo?
- En APA: Hinostroza, T. (28 de abril de 2023). Leyendo archivo línea por línea en ensamblador – MASM32 – Parte 2. Blog de Tito. https://blogdetito.com/2023/04/28/leyendo-archivo-linea-por-linea-en-ensamblador-masm32-parte-2/
- En IEEE: T. Hinostroza. (2023, abril 28). Leyendo archivo línea por línea en ensamblador – MASM32 – Parte 2. Blog de Tito. [Online]. Available: https://blogdetito.com/2023/04/28/leyendo-archivo-linea-por-linea-en-ensamblador-masm32-parte-2/
- En ICONTEC: HINOSTROZA, TIto. Leyendo archivo línea por línea en ensamblador – MASM32 – Parte 2 [blog]. Blog de Tito. Lima Perú. 28 de abril de 2023. Disponible en: https://blogdetito.com/2023/04/28/leyendo-archivo-linea-por-linea-en-ensamblador-masm32-parte-2/
Dejar una contestacion