Portar la utilidad de línea de comando de Go / Rust a D

Hace unos días, en un reddit en "programación", Paulo Henrique Cuchi compartió su experiencia en el desarrollo de una utilidad de línea de comandos en Rust and Go ( traducido al Habré ). La empresa de servicios públicos en cuestión es un cliente de su proyecto favorito Hashtrack. Hashtrack proporciona una API GraphQL con la que los clientes pueden rastrear hashtags de Twitter específicos y obtener una lista de tweets relevantes en tiempo real. Impulsado por un comentario , decidí escribir un puerto en D para demostrar cómo se puede usar D para propósitos similares. Intentaré mantener la misma estructura que usó en la publicación de su blog.



Fuentes en



video de GitHub al hacer clic



Como llegué a D



La razón principal es que la publicación original del blog comparó lenguajes de escritura estática como Go y Rust, e hizo referencias respetuosas a Nim y Crystal, pero no mencionó D, que también entra en esta categoría. Así que creo que hará que la comparación sea interesante.



También me gusta D como idioma y lo he mencionado en varias otras publicaciones de blog.



Ambiente local



El manual contiene amplia información sobre cómo descargar e instalar el compilador de referencia, DMD. Los usuarios de Windows pueden obtener el instalador, mientras que los usuarios de macOS pueden usar homebrew. En Ubuntu, acabo de agregar el repositorio apt y seguí la instalación normal. Con esto, obtienes no solo el DMD, sino también el dub, el administrador de paquetes.



Instalé Rust para poder tener una idea de lo fácil que sería comenzar. Me sorprendió lo fácil que es. Solo necesitaba ejecutar el instalador interactivo , que se encargó del resto. Necesitaba agregar ~ / .cargo / bin a la ruta. Solo tenía que reiniciar la consola para que los cambios surtieran efecto.



Apoyo de editores



Escribí el Hashtrack en Vim sin mucha dificultad, pero probablemente sea porque tengo una idea de lo que está pasando en la biblioteca estándar. Siempre tuve la documentación abierta, porque a veces usé un símbolo que no importé del paquete correcto, o llamé a una función con los argumentos incorrectos. Tenga en cuenta que para la biblioteca estándar, simplemente puede escribir "import std;" y tenlo todo a tu disposición. Sin embargo, para las bibliotecas de terceros, usted está solo.



Tenía curiosidad sobre el estado del conjunto de herramientas, así que busqué complementos para mi IDE favorito, Intellij IDEA. Me pareció que estay lo instalé. También instalé DCD y DScanner clonando sus respectivos repositorios y construyéndolos, luego configuré el complemento IDEA para apuntar a las rutas correctas. Póngase en contacto con el autor de esta publicación de blog para obtener una aclaración.



Me encontré con algunos problemas al principio, pero se solucionaron después de actualizar el IDE y el complemento. Uno de los problemas que encontré fue que no podía reconocer mis propios paquetes y seguía marcándolos como "posiblemente indefinidos". Más tarde descubrí que para que fueran reconocidos, tenía que poner "módulo package_module_name;" en la parte superior del archivo.



Creo que todavía hay un error que no reconoce .length, al menos en mi máquina. Abrí un problema en Github, puedes seguirlo aquísi tienes curiosidad.



Si está en Windows, he escuchado cosas buenas sobre VisualD .



Gestión de paquetes



Dub es el administrador de paquetes de facto en D. Descarga e instala dependencias de code.dlang.org . Para este proyecto, necesitaba un cliente HTTP porque no quería usar cURL. Terminé con dos dependencias, solicitudes y su dependencia, cachetools, que no tiene dependencia propia. Sin embargo, por alguna razón, eligió doce dependencias más:







creo que Dub las usa internamente, pero no estoy seguro de eso.



Rust ha descargado muchas cajas ( Aprox .: 228 ), pero probablemente se deba a que la versión de Rust tiene más funciones que la mía. Por ejemplo, descargó rpassword , una herramienta que oculta los caracteres de la contraseña a medida que los escribe en la terminal, similar a la función getpass de Python.Esta es una de las muchas cosas que no tengo en el código. He añadido soporte getpass para Linux, gracias a esta recomendación . También agregué formato de texto en la terminal, gracias a las secuencias de escape que copié de la fuente Go original.



Bibliotecas



Como tenía poca comprensión de graphql, no tenía idea de por dónde empezar. Una búsqueda de "graphql" en code.dlang.org me llevó a la biblioteca correspondiente, apropiadamente llamada " graphqld ". Sin embargo, después de estudiarlo, me pareció que se parece más a un complemento de vibe.d que a un cliente real, si lo hay.



Después de examinar las solicitudes de red en Firefox, me di cuenta de que para este proyecto simplemente puedo simular solicitudes y transformaciones de graphql que enviaré usando un cliente HTTP. Las respuestas son solo objetos JSON que puedo analizar usando las herramientas proporcionadas por el paquete std.json. Con esto en mente, comencé a buscar clientes HTTP y me decidí por las solicitudes , que es un cliente HTTP fácil de usar, pero lo que es más importante, ha alcanzado un cierto nivel de madurez.



Copié las solicitudes salientes del sniffer y las pegué en archivos .graphql separados, que luego importé y envié con las variables apropiadas. La mayor parte de la funcionalidad se colocó en la estructura GraphQLRequest porque quería insertar los distintos puntos finales y configuraciones en ella según fuera necesario para el proyecto:



Fuente
struct GraphQLRequest
{
    string operationName;
    string query;
    JSONValue variables;
    Config configuration;

    JSONValue toJson()
    {
        return JSONValue([
            "operationName": JSONValue(operationName),
            "variables": variables,
            "query": JSONValue(query),
        ]);
    }

    string toString()
    {
        return toJson().toPrettyString();
    }

    Response send()
    {
        auto request = Request();
        request.addHeaders(["Authorization": configuration.get("token", "")]);
        return request.post(
            configuration.get("endpoint"),
            toString(),
            "application/json"
        );
    }
}




Aquí hay un fragmento de intercambio de paquetes. El siguiente código maneja la autenticación:
struct Session
{
    Config configuration;

    void login(string username, string password)
    {
        auto request = createSession(username, password);
        auto response = request.send();
        response.throwOnFailure();
        string token = response.jsonBody
            ["data"].object
            ["createSession"].object
            ["token"].str;
        configuration.put("token", token);
    }

    GraphQLRequest createSession(string username, string password)
    {
        enum query = import("createSession.graphql").lineSplitter().join("\n");
        auto variables = SessionPayload(username, password).toJson();
        return GraphQLRequest("createSession", query, variables, configuration);
    }
}

struct SessionPayload
{
    string email;
    string password;

    //todo : make this a template mixin or something
    JSONValue toJson()
    {
        return JSONValue([
            "email": JSONValue(email),
            "password": JSONValue(password)
        ]);
    }

    string toString()
    {
        return toJson().toPrettyString();
    }
}




Alerta de spoiler: nunca había hecho esto antes.



Todo sucede así: la función main () crea una estructura de configuración a partir de los argumentos de la línea de comandos y la inyecta en la estructura de la sesión, que implementa la funcionalidad de los comandos de inicio de sesión, cierre de sesión y estado. El método createSession () construye una consulta graphQL leyendo la consulta real del archivo .graphql correspondiente y pasando las variables junto con ella. No quería contaminar mi código fuente con mutaciones y consultas de graphQL, así que las moví a archivos .graphql, que luego importé en tiempo de compilación usando enum e import. Este último requiere una marca de compilador para apuntar a stringImportPaths (que por defecto es view /).



En cuanto al método login (), su única responsabilidad es enviar la solicitud HTTP y procesar la respuesta. En este caso, maneja posibles errores, aunque no con mucho cuidado. Luego almacena el token en un archivo de configuración, que en realidad no es más que un bonito objeto JSON.



El método throwOnFailure no forma parte de la funcionalidad principal de la biblioteca de consultas. En realidad, es una función auxiliar que realiza un manejo rápido y sucio de errores:



void throwOnFailure(Response response)
{
    if(!response.isSuccessful || "errors" in response.jsonBody)
    {
        string[] errors = response.errors;
        throw new RequestException(errors.join("\n"));
    }
}


Dado que D admite UFCS , la sintaxis de throwOnFailure (respuesta) se puede reescribir como response.throwOnFailure (). Esto facilita la integración en otras llamadas a métodos como send (). Es posible que haya abusado de esta funcionalidad durante todo el proyecto.



Procesamiento de errores



D prefiere las excepciones cuando se trata de manejo de errores. El fundamento se explica en detalle aquí . Una de las cosas que me encantan es que los errores no manejados eventualmente aparecerán a menos que se conecten explícitamente. Es por eso que pude alejarme del manejo simplificado de errores. Por ejemplo, en estas líneas:



string token = response.jsonBody
    ["data"].object
    ["createSession"].object
    ["token"].str;
configuration.put("token", token);


Si el cuerpo de la respuesta no contiene un token o ninguno de los objetos que conducen a él, se lanzará una excepción, que aparecerá en la función principal y luego explotará frente al usuario. Si tuviera que usar Go, tendría que tener mucho cuidado con los errores en cada paso. Y, francamente, dado que es molesto escribir if err! = Null cada vez que se llama a la función, estaría muy tentado a simplemente ignorar el error. Sin embargo, mi comprensión de Go es primitiva, y no me sorprendería que el compilador le ladrara por no hacer nada con un retorno de error, así que no dude en corregirme si me equivoco.



El manejo de errores al estilo Rust, como se explica en la publicación original del blog, fue interesante. No creo que haya nada como esto en la biblioteca estándar de D, pero ha habido discusiones sobre la implementación de esto como una biblioteca de terceros.



Websockets



Solo quiero señalar brevemente que no utilicé websockets para implementar el comando watch. Intenté usar el cliente websocket de Vibe.d pero no pudo funcionar con el backend hashtrack porque seguía cerrando la conexión. Al final, lo abandoné a favor de la votación circular, aunque está mal visto. El cliente ha estado funcionando desde que lo probé con otro servidor web, por lo que podría volver a esto en el futuro.



Integración continua



Para CI, configuré dos trabajos de compilación: una compilación de rama regular y una versión maestra para garantizar que se descarguen compilaciones optimizadas de artefactos.









Aprox. Las imágenes muestran el tiempo de montaje. Teniendo en cuenta la carga de dependencias. Reconstruir sin dependencias ~ 4s



Consumo de memoria



Usé el comando / usr / bin / time -v ./hashtrack --list para medir el uso de memoria como se explica en la publicación original del blog. No sé si el uso de la memoria depende de los hashtags que sigue el usuario, pero aquí están los resultados de un programa D compilado con dub build -b release:

Tamaño máximo del conjunto residente (kbytes): 10036

Tamaño máximo del conjunto residente (kbytes): 10164

Tamaño máximo del conjunto residente (kbytes): 9940

Tamaño máximo del conjunto residente (kbytes): 10060

Tamaño máximo del conjunto residente (kbytes): 10008


No está mal. Ejecuté las versiones Go y Rust con mi usuario de hashtrack y obtuve estos resultados:



Go built with go build -ldflags "-s -w":

Tamaño máximo del conjunto residente (kbytes): 13684 Tamaño

máximo del conjunto residente (kbytes): 13820

Tamaño

máximo del conjunto residente (kbytes): 13904 Tamaño

máximo del conjunto residente (kbytes): 13796 Tamaño máximo del conjunto residente (kbytes): 13600

Rust compilado con construcción de carga - lanzamiento:

Tamaño máximo del conjunto residente (kbytes): 9224 Tamaño

máximo del conjunto residente (kbytes): 9192

Tamaño máximo del conjunto residente (kbytes): 9384 Tamaño

máximo del conjunto residente (kbytes): 9132

Tamaño máximo del conjunto residente (kbytes): 9168
Upd: el usuario de Reddit, skocznymroczny, recomendó probar también los compiladores LDC y GDC. Estos son los resultados:

LDC 1.22 compilado por dub build -b release --compiler = ldc2 (después de agregar salida de color y getpass)

Tamaño máximo del conjunto residente (kbytes): 7816

Tamaño máximo del conjunto residente (kbytes): 7912

Tamaño máximo del conjunto residente (kbytes): 7804 Tamaño

máximo del conjunto residente (kbytes): 7832

Tamaño máximo del conjunto residente (kbytes): 7804


D tiene recolección de basura, pero también admite punteros inteligentes y, más recientemente, una metodología de administración de memoria experimental inspirada en Rust. No estoy del todo seguro de qué tan bien se integran estas funciones con la biblioteca estándar, así que decidí dejar que el GC manejara la memoria por mí. Creo que los resultados son bastante buenos considerando que no había pensado en el consumo de memoria mientras escribía el código.



Tamaño de binarios



Rust, cargo build --release: 7.0M



D, dub build -b release: 5.7M



D, dub build -b release --compiler=ldc2: 2.4M



Go, go build: 7.1M



Go, go build -ldflags "-s -w": 5.0M


.. — , , . Windows dub build -b release 2 x64 ( 1.5M x86-mscoff) , Rust Ubuntu18 - openssl, ,





Creo que D es un lenguaje confiable para escribir herramientas de línea de comandos como esta. No fui a dependencias externas muy a menudo porque la biblioteca estándar contenía la mayor parte de lo que necesitaba. Cosas como analizar argumentos de línea de comando, procesamiento JSON, pruebas unitarias, enviar solicitudes HTTP (con cURL ) están disponibles en la biblioteca estándar. Si la biblioteca estándar carece de lo que necesita, existen paquetes de terceros, pero creo que todavía hay margen de mejora en esta área. Por otro lado, si su mentalidad NIH no se inventó aquí, o si desea tener un impacto como desarrollador de código abierto con facilidad, definitivamente le encantará el ecosistema D.



Razones por las que usaría D



  • si



All Articles