El manejo de archivos de texto a bajo nivel, en Windows, resulta problemático si no se tiene unas buenas librerías porque, a pesar de que los archivos de texto son muy comunes, no existe una función apropiada dentro de las API que nos permita leer líneas de texto directamente.
De todo el cargamento de funciones que nos ofrecen las API de Windows para el manejo de archivos, solo la función ReadFile(), y otras similares, nos permite leer datos directamente del disco.
Pero ReadFile() solo realiza lecturas por bloques de tamaño máximo, y no sabe de saltos de línea o delimitadores, así que en principio no nos sirve para leer línea por línea.
En este artículo, presentaré un método que nos permitirá leer efectivamente línea por línea desde un archivo de texto, usando ReadFile(), sin necesidad de tener que cargar todo el contenido del archivo en memoria.
El primer acercamiento para leer archivos de texto con ReadFile(), sería este código que lee solo bloque por bloque:
include \masm32\include\masm32rt.inc
.data
filePath db "texto.txt", 0
buffer db 100 dup(?)
hFile HANDLE ?
bytesRead DWORD ?
.code
start:
; Abrir el archivo
invoke CreateFile, addr filePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
mov hFile, eax ; guardar el identificador de archivo
; Leer el archivo línea por línea
read_loop:
invoke ReadFile, hFile, addr buffer, SIZEOF buffer, addr bytesRead, NULL
cmp bytesRead, 0
je close_file
;invoke StdOut, addr buffer
invoke MessageBox, NULL, ADDR buffer, NULL, NULL
jmp read_loop
close_file: ; Cerrar el archivo
invoke CloseHandle, hFile
invoke ExitProcess, 0
end start
Este es el esquema típico de lectura de archivos usando ReadFile(). Si ensamblamos y ejecutamos este código con MASM32, nos mostrará por pantalla, el archivo «texto.txt» en bloques de 100 bytes cada uno.
Para mostrar mensajes en pantalla, usaremos la API MessageBox().
La función ReadFile() lee bloques de tamaño máximo «SIZEOF buffer» (que en nuestro caso es 100) y deposita los datos en «buffer».
Se podría aumentar o disminuir el tamaño de «buffer» sin ningún problema, pero hay que considerar que:
- Un tamaño muy grande podría desperdiciar espacio si leemos archivos muy pequeños.
- Un tamaño muy pequeño, requeriría hacer muchas lecturas en archivos grandes y afectaría el desempeño.
En este programa estaremos usando ReadFile() en modo síncrono y estaremos verificando la variable «bytesRead» para determinar que hemos llegado al final del archivo (cuando vale 0 después de una lectura).
El código anterior, aunque funciona, tiene muchos problemas, además de que lee bloque por bloque, en lugar de línea por línea. Un primer problema importante es que no existe un delimitador de cadena al final del bloque sino que se está mostrando todo el bloque completo, inclusive el último que, por lo general, queda incompleto.
También sería útil guardar en una variable global la condición de fin de archivo y crear procedimientos separados para la apertura y lectura del archivo.
El siguiente código incluye las mejoras comentadas:
include \masm32\include\masm32rt.inc
.const
LINE_SIZE = 100; longitud máxima de una línea
.data
filePath db "input.tit", 0
hFile HANDLE ? ;Manejador de archivo
buffer db LINE_SIZE dup(?), 0 ;Bolsa de almacenamiento
bytesRead DWORD ? ;Número de bytes leídos
f_eof db 0 ;Bandera de fin de archivo
.code
;Procedimiento para abrir un archivo. Actualiza "hFile" y "f_eof".
open_file PROC
invoke CreateFile, addr filePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
mov hFile, eax ; Guardar el identificador de archivo
mov f_eof, 0 ; Inicializa bandera de fin de archivo
ret
open_file ENDP
;Procedimiento para leer un bloque de datos del archivo "hfile". Actualiza "f_eof".
read_block PROC
invoke ReadFile, hFile, addr buffer, LINE_SIZE, addr bytesRead, NULL
cmp bytesRead, 0
jne no_file_end
mov f_eof, 1 ;Marca bandera
no_file_end:
;Escribe caracter nulo al final de la cadena leída
lea esi, buffer
add esi, bytesRead
mov byte ptr [esi], 0
ret
read_block ENDP
; Programa principal
start:
invoke open_file ;Abrir el archivo
; Leer el archivo línea por línea
read_loop:
invoke read_block ;Lee bloque
;Verifica fin de archivo
cmp f_eof, 1 ;¿Es fin de archivo?
je close_file
;Muesta por pantalla
invoke MessageBox, NULL, ADDR buffer, NULL, NULL
jmp read_loop ;Lee otra vez
close_file: ; Cerrar el archivo
invoke CloseHandle, hFile
invoke ExitProcess, 0
end start
Con este código hemos mejorado la lectura del archivo, incluyendo un carácter NULO al final de los bytes leídos para que la cadena se muestre correctamente, incluyendo el último bloque. Para prevenir el poder alojar siempre el carácter nulo, se ha dejado un byte adicional al final de «buffer».
Pero aún no estamos leyendo línea por línea sino que seguimos leyendo bloque por bloque.
La lectura línea por línea, usando un «buffer» de tamaño fijo, resulta algo complicado, por el hecho de tener que reconocer los diversos saltos de línea que existen (CR, LF o CR-LF), además de tener que lidiar con la lectura parcial de líneas, cuando se usa ReadFile().
El siguiente código ofrece un método para leer un archivo de texto línea por línea, usando un único bloque de tamaño fijo en memoria.
Este programa puede reconocer correctamente los tres casos de delimitadores: CR, LF o CR-LF.
Sin embargo, existe un problema, que aunque poco probable, podría darse (No olvidar a Murphy):
PROBLEMA: Si la cadena tiene un salto de línea de dos bytes, tipo CR-LF, y si este salto de línea queda entre dos bloques consecutivos; al momento de la lectura, se interpretará como dos saltos de línea, haciendo que la rutina lea una línea vacía adicional.
Resolver este «bug», se deja como tarea. Pero como ayuda, se ha dejado un comentario en el código para identificar el punto donde debe solucionarse (en mi opinión, claro).
Por lo demás, y aunque no he hecho pruebas exhaustivas, el código parece funcionar bien. Si descubren algún error, me gustaría que lo comentaran.
Adicionalmente, existe una limitación:
LIMITACIÓN: El tamaño del "buffer" usado para la lectura, define también el tamaño máximo de la línea que se puede leer.
Esta limitación tiene sentido porque todas las líneas leídas deben almacenarse en algún lado y el tamaño del contenedor define el tamaño máximo de lo que puede contener.
Si se leen líneas que sean más largas que «buffer», se leerán como dos o más líneas recortadas.
Ya yendo al grano, el código en cuestión es este:
include \masm32\include\masm32rt.inc
.const
LINE_SIZE = 100 ;Longitud máxima de una línea
.data
filePath db "input.tit", 0
hFile HANDLE ? ;Manejador de archivo
buffer db LINE_SIZE dup(?), 0 ;Bolsa de almacenamiento
bytesRead dword ? ;Número de bytes leídos
f_eof db 0 ;Bandera de fin de archivo
p_eol dword ? ;Puntero a fin de cadena en "buffer".
c_eol db ? ;Caracter EOL encontrado.
.code
;Proc. que posiciona "p_eol" al final de la línea que se encuentra
;actualmente en "buffer". El final de la línea se reconoce por el
;delimitador 0Ah o 0Dh. Si no se encuentra un delimitador de línea
;sale dejando "p_eol" apuntando al final de buffer + 1.
;Actualiza "c_eol".
search_EOL PROC
mov edi, offset buffer
add edi, bytesRead
mov p_eol, offset buffer ;Inicio de buffer
.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
;Proc. para abrir un archivo. Actualiza "hFile" y "f_eof".
open_file PROC
invoke CreateFile, addr filePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
mov hFile, eax ; Guardar el identificador de archivo
mov f_eof, 0 ; Inicializa bandera de fin de archivo
;La cadena a leer queda en "buffer"
;Inicia "p_eol" para forzar a "read_line" a hacer la primera lectura de disco.
mov p_eol, offset buffer + LINE_SIZE
ret
open_file ENDP
;Proc. para leer un bloque de datos del archivo "hfile". Actualiza
;"f_eof".
read_block PROC
;Lectura desde disco
invoke ReadFile, hFile, addr buffer, LINE_SIZE, addr bytesRead, NULL
;Actualiza bandera "f_eof".
cmp bytesRead, 0
jne no_file_end
mov f_eof, 1 ;Marca bandera
no_file_end:
;Escribe caracter nulo al final de la cadena leída
mov edi, offset buffer
add edi, bytesRead
mov byte ptr [edi], 0
;Busca el fin de la primera línea (pueden haberse leído varias).
invoke search_EOL
ret
read_block ENDP
;Versión de "read_block" que lee en la parte superior de "buffer",
;a partir de la posición "bytesRead".
read_block2 PROC
mov ebx, LINE_SIZE
sub ebx, bytesRead
mov edi, offset buffer
add edi, bytesRead
;Salvamos "bytesRead".
mov eax, bytesRead
mov p_eol, eax
;Lectura desde disco
invoke ReadFile, hFile, edi, ebx, addr bytesRead, NULL
;Actualiza bandera "f_eof".
cmp bytesRead, 0
jne no_file_end
mov f_eof, 1 ;Marca bandera
no_file_end:
;Corrige "bytesRead"
mov eax, bytesRead
add eax, p_eol
mov bytesRead, eax
;Escribe caracter nulo al final de la cadena leída
mov edi, offset buffer
add edi, bytesRead
mov byte ptr [edi], 0
;Busca el fin de la primera línea (pueden haberse leído varias).
invoke search_EOL ;Actualiza "p_eol"
ret
read_block2 ENDP
;Proc. que elimina de "buffer", por desplzamiento de bytes, la
;primera línea que está delimitada por "p_eol".
del_line1 PROC
;Se supone que "p_eol" apunta al siguiente primer caracter delimitador
;de fin de línea. Pero el delmitador puede ser de dos bytes.
;Primero vemos si el delimitador es CR-LF.
mov esi, p_eol
inc esi ;Para verificar siguiente byte.
.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
;Calcula en "eax" el número de bytes a eliminar.
sub esi, offset buffer
mov eax, esi ; EAX <- p_eol - offset(buffer)
;Calcula número de bytes a desplazar en ECX
mov ecx, bytesRead
sub ecx, eax
mov bytesRead, ecx ;Aprovechamos para actualizar "bytesRead".
inc ecx ;Corregimos para considerar el chr(0) también.
;Realiza movimiento de bytes
mov edi, offset buffer ;Puntero a destino.
mov esi, edi
add esi, eax ;Puntero a byte origen.
cld ;Dirección -> incrementando ESI/EDI
rep movsb ;Mueve "ecx" veces.
ret
del_line1 ENDP
;Procedimiento para leer una línea de texto
read_line PROC
cmp f_eof, 1 ;¿Es fin de archivo?
je exit_read
;Valida si se necesita leer de disco
lea esi, buffer
add esi, bytesRead ;Ahora "esi" apunta al siguiente byte,
;despúes del bloque leído.
.IF p_eol >= esi ;Terminamos de leer
;Puede que se trate de la primera lectura o una posterior.
invoke read_block ;Leemos bloque de texto.
;Si es que no se encuentra delimitador de línea en el bloque
;leído, se dejará "p_eol" apuntando al final del bloque+1, de
;modo que se considerará que todo el bloque es una línea y se
;dejará para la siguiente lectura leer la parte faltante de
;la línea, si que existe.
.ELSE ;Aún hay datos que leer de buffer
;Eliminamos la primera línea de "buffer".
invoke del_line1 ;Elimina bytes.
;Actualiza "p_eol" al fin de la siguiente línea.
invoke search_EOL
;Verifica la línea identificada, para ver si está completa.
lea esi, buffer
add esi, bytesRead ;Ahora "esi" apunta al siguiente byte,
;despúes del bloque leído.
; .IF p_eol+1 == esi ;*** Este caso crítico habría que
; ;verificarlo porque podría corresponder
; ;a un CR-LF que quedó entre dos bloques.
; .ELSE
.IF p_eol == esi ;No se encontró delimitador en lo que queda del bloque.
;Por si hay más bloques por leer.
invoke read_block2 ;Leemos bloque de texto.
;Aquí ya se actualizó "p_eol" y "bytesRead"
.ENDIF
; .ENDIF
.ENDIF
exit_read:
ret
read_line ENDP
;------------ Programa principal ---------------
start:
invoke open_file ;Abrir el archivo
; Leer el archivo línea por línea
read_loop:
;Verifica fin de archivo
cmp f_eof, 1 ;¿Es fin de archivo?
je close_file
;Lee bloque
invoke read_line
;Muesta por pantalla
invoke MessageBox, NULL, offset buffer, NULL, NULL
jmp read_loop ;Lee otra vez
close_file: ;Cerrar el archivo
invoke CloseHandle, hFile
invoke ExitProcess, 0
end start
El método que usa este programa consiste en ir identificando las líneas dentro de «buffer» para ir marcándolas con un carácter nulo. Luego, en la siguiente lectura, si es que aún quedan líneas por leer dentro de «buffer», se elimina la línea anterior desplazando los bytes para que quede accesible la siguiente línea al inicio de «buffer».
Este proceso se repite hasta que ya no queden líneas por leer dentro de «buffer» o queden líneas incompletas. En este caso, se procede a hacer una nueva lectura con la API ReadFile() para completar la línea incompleta (subrutina «read_block2»).
Este programa es bastante rápido, pero siempre queda lugar para mejoras. No me he basado en ningún código previo, sino que he codificado directamente el método que he diseñado, sin ser un experto en la arquitectura Intel o el ensamblador MASM32.
Otra alternativa para simplificar el trabajo de lectura línea por línea podría ser leer todo el contenido del archivo en una zona de memoria dinámica, y esa solución sería bastante rápida pero podría ser perjudicial cuando se leen archivos muy grandes.
En la siguiente parte de este artículo exploraré esta otra alternativa.
¿Cómo citar este artículo?
- En APA: Hinostroza, T. (26 de abril de 2023). Leyendo archivo línea por línea en ensamblador – MASM32 – Parte 1. Blog de Tito. https://blogdetito.com/2023/04/26/leyendo-archivo-linea-por-linea-en-ensamblador-masm32-parte-1/
- En IEEE: T. Hinostroza. (2023, abril 26). Leyendo archivo línea por línea en ensamblador – MASM32 – Parte 1. Blog de Tito. [Online]. Available: https://blogdetito.com/2023/04/26/leyendo-archivo-linea-por-linea-en-ensamblador-masm32-parte-1/
- En ICONTEC: HINOSTROZA, TIto. Leyendo archivo línea por línea en ensamblador – MASM32 – Parte 1 [blog]. Blog de Tito. Lima Perú. 26 de abril de 2023. Disponible en: https://blogdetito.com/2023/04/26/leyendo-archivo-linea-por-linea-en-ensamblador-masm32-parte-1/
Dejar una contestacion