Bienvenido al sector de Internet que no sabías que existía.

miércoles, 7 de abril de 2021

Programas autorreplicantes (guía)

 Esta es una guía para escribir un programa que, al ejecutarlo, imprime su propio código. (No hablo de que sale por una impresora en una hoja, imprime en pantalla...)

¿Es posible hacer esto? Sí. Por algo te hice esta guía. Empecemos.

Introducción

Este tipo de programas (llamados quine, o ouroboros en inglés) se puede hacer de muchas formas. Hay gente muy loca que inventa todo tipo de formatos para llevarlos a cabo, variando entre lenguajes, por supuesto.
En Python se puede ver un ejemplo muy, muy corto de un programa que se autorreplica al ejecutarse:

s='s=%r;print(s%%s)';print(s%s)

Ese programa de una sola línea es código perfectamente válido en Python, y al ejecutarlo imprime en pantalla exactamente los mismos caracteres que conforman su código fuente.
Puede ser un poco raro de entender, pero su funcionamiento es en realidad sencillo (valiéndose de algunas curiosidades sintácticas) y sigue los mismos principios que voy a comentar en la guía.

No voy a explicar ese programa, sin embargo.

Idea principal

Todo quine típicamente debería tener estos componentes, de alguna manera:
  • Su código
  • Una representación de su código como texto
  • Una instrucción que imprima esa representación
En el caso anterior en Python, la variable s es una cadena (string) que contiene el código fuente del programa, no de forma exacta, sino de una forma que al printear (con ciertas modificaciones) producirá el código fuente.
Veamos cómo replicar esto en un programa en C.

Pasos a seguir

Voy a dar una serie de pasos para recrear esto en el lenguaje C. Es relativamente sencillo y mecánico si consideramos algunas cosas.

  1. Escribir un programa que haga algo. Nos vamos a dar la libertad de hacer uno que no haga nada, excepto printearse a sí mismo, en esta ocasión.
  2. Crear una variable que contenga TODO el código fuente, reemplazando las nuevas líneas, tabulaciones, comillas dobles por %c, y a sí misma por %s. Manejaremos esto posteriormente.
  3. Agregar una línea de printf() inmediatamente antes del fin del programa. Esta será la encargada de printear el código fuente. Hay que agregarla en la cadena también.
  4. Acomodar los argumentos de printf() para que reemplace cada %c por el carácter necesario, y %s por la cadena que definimos.
    Escribiremos su representación en ASCII para cada %c, como número decimal, para mayor facilidad. Estos son carácteres que nos complican especialmente la vida en la generación de quines, ya que se representan como secuencias de escape en las cadenas (\n, \t, \" respectivamente. Mucho lío manejar esto, porque la barra \ también necesita ser "escapada" en la representación. Mejor ASCII):
    1. nueva línea = 10
    2. tabulación = 9
    3. comillas dobles = 34
  5. Escribir los argumentos del printf() en la cadena de texto, ejecutar el programa, y gozar.
Ahora vamos a ver un ejemplo práctico de esto.

Manos a la obra

Empecemos escribiendo una plantilla básica de un programa en C:

#include <stdio.h>
    int main() {
        return 0;
    }
Uno podría hacer que este programa haga algo, pero decidimos dejarlo así para esta guía. Si se ejecuta eso, solamente retorna 0 y termina su ejecución.

Vayamos al paso 2 y agreguemos una variable para contener la cadena. Dejémosla vacía por ahora.


        #include <stdio.h>
            int main() {
                char cadena[]="";
                return 0;
            }
    

Agreguemos la línea del printf() a continuación:


        #include <stdio.h>
            int main() {
                char cadena[]="";
                printf(/* para completar */);
                return 0;
            }
    

Y pasemos a ensuciarnos las manos. Vamos a completar la cadena con todo el código fuente. Vayamos de a poco.
Vamos a ver cómo debería lucir esa cadena solita:


        "#include <stdio.h>%cint main() {%c%cchar cadena[]=%c%s%c;%c%cprintf(/* para completar */);%c%creturn 0;%c}"
    

Pero, ¿quejesto?
No entrar en pánico.
Es el programa, puesto en una línea sola, y cambiando lo que dije que había que cambiar:
  • Saltos de línea por %c
  • Tabulaciones por %c
  • Comillas dobles por %c
  • La cadena en sí por %s
No te mentí. Yo te había avisado de esto. 👀

Entonces, pongamos eso en la variable que creamos.


            #include <stdio.h>
                int main() {
                    char cadena[]="#include <stdio.h&t;%cint main() {%c%cchar cadena[]=%c%s%c;%c%cprintf(/* para completar */);%c%creturn 0;%c}";
                    printf(/* para completar */);
                    return 0;
                }
        

Completemos el printf() ahora.
Es fácil: primero, va la cadena. Después, el valor de cada elemento a reemplazar en el formato de la cadena (son los %c y el %s).

Como vemos que el primer %c corresponde a un salto de línea, el primer número será un 10.
El segundo es otro salto de línea, otro 10.
El tercero es un tab, entonces ponemos 9. Etc.

Esa línea debería quedar así:


            printf(cadena,10,10,9,34,cadena,34,10,9,10,9,10);
        

Vemos que para el lugar de %s pusimos simplemente el nombre de la cadena, y para las comillas, 34.

Entonces lo ponemos en el código:

            #include <stdio.h>
                int main() {
                    char cadena[]="#include <stdio.h>%cint main() {%c%cchar cadena[]=%c%s%c;%c%cprintf(/* para completar */);%c%creturn 0;%c}";
                    printf(cadena,10,10,9,34,cadena,34,10,9,10,9,10);
                    return 0;
                }
        
Y no olvidemos de cambiar lo que dice /* para completar */ en la cadena también:

            #include <stdio.h>
                int main() {
                    char cadena[]="#include <stdio.h>%cint main() {%c%cchar cadena[]=%c%s%c;%c%cprintf(cadena,10,10,9,34,cadena,34,10,9,10,9,10);%c%creturn 0;%c}";
                    printf(cadena,10,10,9,34,cadena,34,10,9,10,9,10);
                    return 0;
                }
        

Y como dice el último paso, queda compilar, ejecutar el programa, y gozar. Porque ya terminamos.

Se puede compilar (usando quizás gcc programa.c), y ejecutar, para ver que efectivamente se imprime en pantalla exactamente el código del programa.

Incluso se puede ejecutar esta serie de comandos (en alguna plataforma basada en Unix) para verificar que funciona. Con un fuente hola.c:
gcc hola.c -o hola.out touch nuevo.c ./hola.out > nuevo.c gcc nuevo.c -o nuevo.out ./nuevo.out
Y debería printear nuevamente lo mismo.

Quiero más

Te dejo algunos links.

Chau

Nos vemos.