[JAVA] J'ai essayé d'étudier le mécanisme d'Emscripten en l'utilisant avec un solveur allemand

J'ai étudié le mécanisme d'Emscripten en exécutant le solveur allemand de type backtrack implémenté en C dans l'environnement JavaScript en utilisant Emscripten. , Est l'histoire.

Qu'est-ce qu'Emscripten

emscripten utilise LLVM / Clang pour afficher les programmes C et C ++ dans les navigateurs et nodejs/[iojs](https: C'est un système qui permet l'exécution dans un environnement JavaScript tel que //iojs.org/).

Basé sur le byte code de LLVM qui compile la source C / C ++, le ** programme de conversion pour s'exécuter sur JavaScript et la partie de l'environnement d'exécution pour l'exécuter ** sont compatibles avec gcc, make, etc. Amélioré ʻemcc](https://github.com/kripken/emscripten/blob/master/emcc) et [ʻemmake ) Et d'autres ** lignes de commande ** sont fournies. Le premier est géré par nodejs et le second par python.

La ** bibliothèque d'exécution (fonction) utilisée en C / C ++ telle que libc est conçue pour être émulée du côté JavaScript **. .. Sortie standard vers la console, dessin de SDL/OpenGL, etc. sur l'environnement ou le navigateur nodejs / iojs L'implémentation JS à réaliser, etc. est attachée en standard à emscripten (par exemple, l'implémentation émulée de libc est [/ usr / local / opt / emscripten / libexec / src / library.js](https: //) github.com/kripken/emscripten/blob/master/src/library.js)).

Préférences Emscripten

Dans l'environnement OSX, j'ai installé emscripten par homebrew (brew install emscripten).

Quand emscripten est inséré, des commandes telles que ʻemcc(commande compatible gcc / clang) sont ajoutées, et quand la commande est exécutée pour la première fois, un fichier~ / .emscripten` avec l'environnement défini est créé.

Cependant, sous OSX, le contenu n'est pas reflété correctement, comme l'utilisation de la commande clang attachée au système actuel. Par conséquent, modifiez-le à nouveau comme suit.

# ~/.emscripten
EMSCRIPTEN_ROOT = os.path.expanduser(
    os.getenv('EMSCRIPTEN') or 
    '/usr/local/opt/emscripten/libexec')
LLVM_ROOT = os.path.expanduser(
    os.getenv('LLVM') or 
    '/usr/local/opt/emscripten/libexec/llvm/bin')

NODE_JS = os.path.expanduser(
    os.getenv('NODE') or 
    '/usr/local/opt/node/bin/node')

TMP_DIR = '/tmp'
COMPILER_ENGINE = NODE_JS
JS_ENGINES = [NODE_JS]

En spécifiant le package homebrew de cette manière, même si le package emscripten ou nodejs installé par la mise à niveau homebrew est mis à jour, il peut être exécuté sans réinitialisation à chaque fois. ..

Si vous exécutez la commande ʻemcc` vide avec ce paramètre, vous obtiendrez le résultat suivant s'il est correctement défini.

$ emcc
WARNING  root: (Emscripten: settings file has changed, clearing cache)
INFO     root: (Emscripten: Running sanity checks)
WARNING  root: no input files

Ecrire un solveur allemand en langage C (C11)

Tout d'abord, décrivez un solveur allemand carré 9x9 de type backtrack dans Spécifications C11. Séparez les fichiers dans la partie bibliothèque et la partie main du code afin qu'ils ne puissent être utilisés que par la bibliothèque ultérieurement.

// libsudoku.c
#include <stdio.h>

// helpers for board data
static inline unsigned masks(unsigned i) {return i ? 1 << (i - 1) : 0;}
static inline unsigned row(unsigned i) {return i / 9;}
static inline unsigned col(unsigned i) {return i % 9;}
static inline unsigned blk(unsigned i) {return i / 27 * 3 + i % 9 / 3;}
extern void output(unsigned board[])
{
  char buffer[(9 + 3) * (9 + 3)];
  char* cur = buffer;
  for (unsigned y = 0; y < 9; y++) {
    for (unsigned x = 0; x < 9; x++) {
      *cur++ = board[y * 9 + x] > 0 ? board[y * 9 + x] + '0' : '.';
      if (x % 3 == 2) *cur++ = x == 8 ? '\n' : '|';
    }
    if (y == 8) {
      *cur++ = '\0';
    } else if (y % 3 == 2) {
      for (unsigned i = 0; i < 11; i++) *cur++ = i % 4 == 3 ? '+' : '-';
      *cur++ = '\n';
    }
  }
  puts(buffer);
}

// sudoku solver
typedef void (*sudoku_cb)(unsigned board[]);
typedef struct {
  unsigned board[81];
  unsigned rows[9], cols[9], blks[9];
  sudoku_cb callback;
} sudoku_t;

static void sudoku_init(sudoku_t* s, unsigned board[])
{
  for (unsigned i = 0; i < 81; i++) {
    const unsigned mask = masks(board[i]);
    s->rows[row(i)] |= mask, s->cols[col(i)] |= mask, s->blks[blk(i)] |= mask;
    s->board[i] = board[i];
  }
}

static void sudoku_solve(sudoku_t* s, unsigned i)
{
  if (i == 81) {
    s->callback(s->board);
  } else if (s->board[i] != 0) {
    sudoku_solve(s, i + 1);
  } else {
    const unsigned r = row(i), c = col(i), b = blk(i);
    const unsigned used = s->rows[r] | s->cols[c] | s->blks[b];
    for (unsigned v = 1; v <= 9; v++) {
      const unsigned mask = masks(v);
      if (used & mask) continue;
      s->board[i] = v;
      s->rows[r] |= mask, s->cols[c] |= mask, s->blks[b] |= mask;
      sudoku_solve(s, i + 1);
      s->rows[r] &= ~mask, s->cols[c] &= ~mask, s->blks[b] &= ~mask;
      s->board[i] = 0;
    }
  }
}

extern void sudoku(unsigned board[], sudoku_cb callback)
{
  sudoku_t s = {
    .board = {0}, .rows = {0}, .cols = {0}, .blks = {0}, .callback = callback};
  sudoku_init(&s, board);
  sudoku_solve(&s, 0);
}
// sudoku-main.c

#include <stdio.h>

extern void output(unsigned board[]);
typedef void (*sudoku_cb)(unsigned board[]);
extern void sudoku(unsigned board[], sudoku_cb callback);

// example problem from http://rosettacode.org/wiki/Sudoku
static unsigned problem[] = {
  8, 5, 0, 0, 0, 2, 4, 0, 0,
  7, 2, 0, 0, 0, 0, 0, 0, 9,
  0, 0, 4, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 1, 0, 7, 0, 0, 2,
  3, 0, 5, 0, 0, 0, 9, 0, 0,
  0, 4, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 8, 0, 0, 7, 0,
  0, 1, 7, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 3, 6, 0, 4, 0
};

extern int main(int argc, char* argv[])
{
  if (argc <= 1) {
    puts("[problem]");
    output(problem);
    puts("[solutions]");
    sudoku(problem, output);
  } else {
    for (int i = 1; i < argc; i++) {
      char ch = -1;
      for (int c = 0; c < 81; c++) {
        if (ch == '\0') {
          problem[c] = 0;
        } else {
          ch = argv[i][c];
          problem[c] = '0' <= ch && ch <= '9' ? ch - '0' : 0;
        }
      }
      puts("[problem]");
      output(problem);
      puts("[solution]");
      sudoku(problem, output);
    }
  }
  return 0;
}

Ce code est un programme qui peut être normalement compilé / exécuté avec gcc / clang comme indiqué ci-dessous.

$ clang -std=c11 -Wall -Wextra libsudoku.c sudoku-main.c -o sudoku
$ ./sudoku 
[problem]
85.|..2|4..
72.|...|..9
..4|...|...
---+---+---
...|1.7|..2
3.5|...|9..
.4.|...|...
---+---+---
...|.8.|.7.
.17|...|...
...|.36|.4.

[solutions]
859|612|437
723|854|169
164|379|528
---+---+---
986|147|352
375|268|914
241|593|786
---+---+---
432|981|675
617|425|893
598|736|241

Compilez un programme C avec Emscripten et exécutez-le avec iojs

Vous pouvez le compiler en code JavaScript simplement en changeant la partie clang dans la construction ci-dessus en ʻemcc et en ** ajoutant .js` au nom du fichier de sortie **. Le fichier js généré est ** autonome et exécutable sur nodejs / iojs **.

$ emcc -std=c11 -Wall -Wextra libsudoku.c sudoku-main.c -o sudoku.js
$ iojs sudoku.js 
[problem]
85.|..2|4..
72.|...|..9
..4|...|...
---+---+---
...|1.7|..2
3.5|...|9..
.4.|...|...
---+---+---
...|.8.|.7.
.17|...|...
...|.36|.4.

[solutions]
859|612|437
723|854|169
164|379|528
---+---+---
986|147|352
375|268|914
241|593|786
---+---+---
432|981|675
617|425|893
598|736|241


Appeler une fonction dans une bibliothèque compilée avec Emscripten à partir de code JavaScript

Le code généré par Emscripten n'est pas ** du code JS qui peut être utilisé comme une fonction JavaScript normale (comme un traducteur tel que CoffeeScript) **. Ce qui est généré est une combinaison du bytecode LLVM (en supposant le modèle d'exécution du langage C) et de son environnement d'exécution. Par exemple, un mécanisme de gestion quantitative de zone de mémoire tel que HEAP existe dans le code JS.

C'est la même chose même si la bibliothèque est générée et que la fonction «externe» que possède la bibliothèque est celle qui exécute le code d'octet en tant que fonction ** C sur l'environnement d'exécution intégré dans la bibliothèque. Il est **.

Par conséquent, afin d'utiliser le code généré par Emscripten comme bibliothèque pour JavaScript, une certaine ingéniosité est requise à certains endroits. Dans le document officiel, Interagir avec le code, les informations nécessaires sont décrites. Cependant, si vous ne reconnaissez pas la différence majeure qu'il s'agit de ** moyens d'accès à l'environnement d'exécution du programme C dans la bibliothèque **, vous en serez accro.

Compiler en tant que bibliothèque nodejs / iojs

Pour générer libsudoku.js à utiliser comme bibliothèque sur nodejs / iojs à partir de libsudoku.c, vous devez spécifier les options attachées -s EXPORTED_FUNCTIONS et -s RESERVED_FUNCTION_POINTERS comme suit. il y a.

$ emcc -Wall -Wextra -std=c11 libsudoku.c -o libsudoku.js \
      -s EXPORTED_FUNCTIONS="['_sudoku','_output']" \
      -s RESERVED_FUNCTION_POINTERS=20

Dans l'ancien ʻEXPORTED_FUNCTIONS, spécifiez le nom de la fonction ʻextern en C pour l'appeler du côté JavaScript. Cependant, au lieu de lister les noms des fonctions C tels quels, listez les noms ** précédés de _.

Ce dernier, RESERVED_FUNCTION_POINTERS, est une variable nécessaire pour ** lorsque vous devez appeler une fonction qui passe un pointeur vers une fonction de rappel, telle que la fonction sudoku (tableau, callback), du côté JavaScript **. Ici, réservez le nombre de pointeurs de fonction qui peuvent être définis sur l'environnement d'exécution de code d'octet afin de rappeler la fonction JavaScript. Puisque libsudoku.js n'est pas exécuté en parallèle, 1 est suffisant pour libsudoku.js, mais 20 est spécifié ici.

Écrire du code JavaScript qui appelle la bibliothèque Emscripten

La documentation dit que vous pouvez utiliser ccall et cwrap, mais ils ne listent que lorsque la valeur simple number est échangée, et ont été générés pour utiliser des tableaux et des fonctions comme arguments. J'ai analysé le code et fait des essais et des erreurs.

Comment appeler en passant un tableau en argument avec ccall

Tout d'abord, le wrapper qui appelle void output (unsigned board []) de libsudoku.c est le suivant.

var libsudoku = require("./libsudoku.js");

// use ccall to call emscripten C function
var output = function (board) {
    // unsigned[] as Uint8Array view of Uint32Array
    var unsignedBoard = new Uint8Array(new Uint32Array(board).buffer);
    // ccall(cfuncname, return_type, argument_types, arguments)
    // type: "string", "number", "array" or undefined as return type
    return libsudoku.ccall("output", undefined, ["array"], [unsignedBoard]);
};

Ici, nous appelons ccall comme valeur de tableau d'Emscripten avec ʻArrayde JavaScript. L'argument estlibrary.ccall (nom de la fonction, type de retour, liste des types d'arguments, liste des valeurs des arguments). Ce nom de fonction sera ** le nom de la fonction en C sans le début _` **, pas l'option de compilation.

Si la valeur de retour est «void», spécifiez «non défini». Le type d'argument peut être soit " number ", " string " ou " array ". Cette spécification de type n'est pas le type dans la déclaration de fonction C, mais le ** type de valeur JavaScript ** qui est réellement passé lors de son appel et de son exécution.

Le type de pointeur de l'argument de ʻoutput () est ʻunsigned (int), mais ** Emscripten semble traiter le type ʻintcomme un entier 32 bits **. Et ** l'ordre des octets semble hériter de la dépendance de l'environnement CPU deTypedArray` tel qu'il est ** (le nouveau Uint32Array (tampon), etc. est décrit dans le code généré).

Si le tableau 32 bits est géré en langage C, l'argument de type ** " array " lors de l'appel avec ccall doit être 8 bits ʻInt8Array ou ʻUint8Array **. En regardant l'implémentation ccall dans la bibliothèque, l'argument passé lors de l'appel ** copie sa valeur dans la zone mémoire de la bibliothèque et provoque l'exécution de bytecode **. La "chaîne" et le "tableau" indiquent la méthode de copie. Dans le cas de «« array »«, il est copié avec «HEAP8 [...] = array [i]», donc si vous passez «Uint32Array» etc. à «ccall» tel quel, il sera tronqué à 8 bits. Au fait, «string» est passé à C sous la forme d'une chaîne de caractères UTF8 «char».

Comment appeler une fonction qui passe un pointeur de fonction

Le code suivant implémente un wrapper pour sudoku (board, callback) en utilisant un autre cwrap. ccall (nom de fonction, type de retour, liste de types d'argument, liste de valeurs d'argument) est équivalent à cwrap (nom de fonction, type de retour, liste de types d'argument) (liste de valeurs d'argument). Lorsque vous appelez la fonction emscripten plusieurs fois, il est préférable de mettre en cache la fonction retournée par cwrap et de l'appeler en ne passant que l'argument.

Le point ici est que la fonction de rappel JavaScript est définie dans l'environnement d'exécution de code d'octet et que sa valeur de pointeur est passée en tant qu'argument de la fonction créée par cwrap comme" nombre ".

// use cwrap to call emscripten C function with callback
var sudoku = function sudoku(board, callback) {
    // NOTE: For using addFunction(),
    //    emcc option "-s REQUIRED_FUNCTION_POINTERS=10" (more than 1) required
    var callbackPtr = libsudoku.Runtime.addFunction(function (resultPtr) {
        var r = new Uint32Array(libsudoku.HEAPU8.buffer, resultPtr, 81);
        // NOTE: copy memory value as array for async use in callback(like IO)
        callback([].slice.call(r));
    });
    var unsignedBoard = new Uint8Array(new Uint32Array(board).buffer);
    sudoku._cfunc(unsignedBoard, callbackPtr);
    libsudoku.Runtime.removeFunction(callbackPtr);
};
// pointer as a "number"
sudoku._cfunc = libsudoku.cwrap("sudoku", undefined, ["array", "number"]);

Utilisez ptr = library.Runtime.addFunction (function) pour définir la fonction JavaScript dans l'environnement bytecode et recevoir sa valeur de pointeur. Le nombre d'arguments à la compilation RESERVED_FUNCTION_POINTERS fait référence au nombre de fonctions JavaScript qui peuvent être enregistrées avec ʻaddFunction` en même temps.

Les arguments passés à la fonction appelée dans le callback sont les nombres exacts dans l'environnement d'exécution (pas les valeurs dans ccall etc.). ** S'il s'agit d'un pointeur, il sera utilisé comme valeur de décalage de HEAP (library.HEAPU8 etc.) lors de l'exécution **. Pour masquer cela, ** la fonction de rappel a également besoin d'un wrapper **. Dans ce wrapper de rappel, nous créons un ʻArray` régulier à partir de l'offset de mémoire et appelons le rappel réel.

var callbackPtr = libsudoku.Runtime.addFunction(function (resultPtr) {
    var r = new Uint32Array(libsudoku.HEAPU8.buffer, resultPtr, 81);
    callback([].slice.call(r));
});

Puisque le result passé dans le callback est ʻunigned [81] sur C, découpez un tableau de 81 entiers 32 bits de HEAP avec ʻUint32Array, convertissez-le en un ʻArray` normal, et utilisez la fonction de rappel. J'essaye d'appeler. «ccall» et «cwrap» effectuent un tel accès HEAP à l'intérieur pour convertir entre les objets JavaScript et les valeurs de mémoire.

Non limité au pointeur de fonction retourné par library.Runtime.addFunction (f), la valeur du pointeur est passée sous forme de "nombre" à l'argument de la fonction de "ccall" ou "cwrap".

Une fois l'appel de fonction terminé, supprimez-le avec library.Runtime.removeFunction (ptr) et ouvrez un emplacement où ʻaddFunction` peut être fait. Les bibliothèques Emscripten nécessitent cette ** gestion des ressources manuellement ** comme en C.

partie principale et exécution

Le code qui utilise le wrapper ci-dessus et effectue le même traitement que sudoku-main.c est le suivant.

// sudoku-call-libsudoku.js 
var libsudoku = require("./libsudoku.js");

var output = function (board) {
    var unsignedBoard = new Uint8Array(new Uint32Array(board).buffer);
    return libsudoku.ccall("output", undefined, ["array"], [unsignedBoard]);
};

var sudoku = function sudoku(board, callback) {
    var callbackPtr = libsudoku.Runtime.addFunction(function (resultPtr) {
        var r = new Uint32Array(libsudoku.HEAPU8.buffer, resultPtr, 81);
        callback([].slice.call(r));
    });
    var unsignedBoard = new Uint8Array(new Uint32Array(board).buffer);
    sudoku._cfunc(unsignedBoard, callbackPtr);
    libsudoku.Runtime.removeFunction(callbackPtr);
};
sudoku._cfunc = libsudoku.cwrap("sudoku", undefined, ["array", "number"]);

// main
if (process.argv.length <= 2) {
    // example problem from http://rosettacode.org/wiki/Sudoku
    var problem = [
        8, 5, 0, 0, 0, 2, 4, 0, 0,
        7, 2, 0, 0, 0, 0, 0, 0, 9,
        0, 0, 4, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 0, 7, 0, 0, 2,
        3, 0, 5, 0, 0, 0, 9, 0, 0,
        0, 4, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 8, 0, 0, 7, 0,
        0, 1, 7, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 3, 6, 0, 4, 0
    ];
    console.log("[problem]");
    output(problem);
    console.log("[solution]");
    sudoku(problem, output);
} else {
    for (var i = 2; i < process.argv.length; i++) {
        var problem = Array(81);
        for (var j = 0; j < 81; j++) {
            var ch = process.argv[i].charCodeAt(j);
            problem[j] = 0 <= ch && ch <= 9 ? v : 0;
        }
        console.log("[problem]");
        output(problem);
        console.log("[solution]");
        sudoku(problem, output);
    }
}

Comme indiqué ci-dessous, il peut être exécuté tel quel sans ** bibliothèque externe requise **, tout comme lors de la compilation avec main.

$ iojs sudoku-call-libsudoku.js
[problem]
85.|..2|4..
72.|...|..9
..4|...|...
---+---+---
...|1.7|..2
3.5|...|9..
.4.|...|...
---+---+---
...|.8.|.7.
.17|...|...
...|.36|.4.

[solution]
859|612|437
723|854|169
164|379|528
---+---+---
986|147|352
375|268|914
241|593|786
---+---+---
432|981|675
617|425|893
598|736|241

Analyse de la vitesse d'exécution

Le temps d'exécution de sudoku pour une exécution native, sudoku.js qui convertit tout le code C et sudoku-call-libsudoku.js qui utilise la bibliothèque libsudoku.js est le suivant.

$ time ./sudoku
real	0m0.020s
user	0m0.015s
sys	0m0.002s
$ time iojs sudoku-call-libsudoku.js
real	0m0.355s
user	0m0.313s
sys	0m0.037s
$ time iojs sudoku.js
real	0m0.888s
user	0m0.312s
sys	0m0.045s

Il semble qu'il est plus rapide d'utiliser uniquement la bibliothèque que d'implémenter main en C (bien que la fonction de bibliothèque externe évite printf etc. et n'utilise que put ()).

À propos, le temps d'exécution dans Code implémenté en arrière avec JavaScript est

$ time iojs sudoku-backtrack.js
real	0m0.723s
user	0m0.684s
sys	0m0.030s

est devenu.

Résoudre la carte une seule fois est plus rapide que de convertir tout le code C, probablement parce que le coût de la partie principale a un effet important. Il a fallu deux fois plus de temps pour utiliser la bibliothèque, et on peut dire que la partie solveur a été doublée avec Emscripten.

Question: Comment passer struct par valeur avec ccall

Avec ccall et cwrap, il semble qu'il n'y ait aucun moyen de passer une ** valeur struct ** (non passée par un pointeur), telle que vec4 avec quatre floats.

Référence: options d'optimisation et de débogage Emscripten

Semblables à gcc / clang, les options d'optimisation et de débogage incluent O0-3 et g1-4, mais leurs significations sont différentes et ont des significations spécifiques à Emscripten.

--O0: Aucune optimisation (par défaut) --O1: le code de débogage est supprimé --O2: la sortie est minifiée (le fichier .js.mem est généré) --O3: optimisation supplémentaire (fichier légèrement plus petit que O2)

Le fichier .js.mem généré à la fois par O2 et O3 est un fichier indispensable pour exécuter le code .js généré.

L'option de débogage est utilisée en combinaison avec l'option d'optimisation après O2 pour supprimer minify **.

--g1: Enregistrer vide --g2: enregistrer le nom de la fonction --g3: enregistrer le nom de la variable --g4: Générer la carte source (fichier .map)

Lien vers le code source

Le code source complet est dans l'essentiel avec Makefile.

Recommended Posts

J'ai essayé d'étudier le mécanisme d'Emscripten en l'utilisant avec un solveur allemand
J'ai essayé de construire l'environnement petit à petit en utilisant docker
J'ai essayé de résoudre le problème de la "sélection multi-étapes" avec Ruby
J'ai essayé de créer un environnement de serveur UML Plant avec Docker
J'ai essayé de vérifier le fonctionnement du serveur gRPC avec grpcurl
J'ai essayé de me permettre de définir le délai pour le client Android UDP
J'ai essayé de visualiser l'accès de Lambda → Athena avec AWS X-Ray
J'ai essayé de mesurer et de comparer la vitesse de Graal VM avec JMH
J'ai essayé d'utiliser le profileur d'IntelliJ IDEA
J'ai essayé de comparer la technologie d'infrastructure des ingénieurs ces jours-ci avec la cuisine.
J'ai essayé d'utiliser la fonction Server Push de Servlet 4.0
05. J'ai essayé de supprimer la source de Spring Boot
J'ai essayé de réduire la capacité de Spring Boot
J'ai essayé d'implémenter la fonction similaire par communication asynchrone
J'ai essayé d'augmenter la vitesse de traitement avec l'ingénierie spirituelle
J'ai essayé de résumer les bases de kotlin et java
J'ai brièvement résumé la grammaire de base de Ruby
J'ai essayé de créer un environnement de WSL2 + Docker + VSCode
J'ai essayé de démarrer avec Swagger en utilisant Spring Boot
J'ai essayé d'utiliser la bibliothèque CameraX avec Android Java Fragment
J'ai essayé de toucher l'application de gestion d'actifs en utilisant l'émulateur du grand livre distribué Scalar DLT
Lorsque j'ai essayé d'exécuter Azure Kinect DK avec Docker, il a été bloqué par le CLUF
[Metal] J'ai essayé de comprendre le flux jusqu'au rendu avec Metal
[Rails] Implémentation de la fonction de catégorie multicouche en utilisant l'ascendance "J'ai essayé de créer une fenêtre avec Bootstrap 3"
J'ai essayé d'utiliser JOOQ avec Gradle
J'ai essayé de me connecter à MySQL en utilisant le modèle JDBC avec Spring MVC
J'ai essayé d'implémenter la fonction de prévisualisation d'image avec Rails / jQuery
J'ai essayé d'utiliser la fonction de cache d'Application Container Cloud Service
J'ai essayé d'approfondir ma compréhension de l'orientation des objets de n%
J'ai essayé d'interagir avec Java
Je veux limiter l'entrée en réduisant la plage de nombres
J'ai essayé de résumer les méthodes de Java String et StringBuilder
J'ai essayé d'en faire une URL arbitraire en utilisant l'imbrication de routage
[Java] J'ai essayé de faire un labyrinthe par la méthode de creusage ♪
J'ai essayé d'afficher le calendrier sur la console Eclipse en utilisant Java.
J'ai essayé de résoudre le problème de Google Tech Dev Guide
J'ai essayé d'utiliser Google HttpClient de Java
J'ai essayé de créer un exemple de programme en utilisant le problème du spécialiste des bases de données dans la conception pilotée par domaine
J'ai créé et défini mon propre dialecte avec Thymeleaf et j'ai essayé de l'utiliser
J'ai essayé de connecter le compteur de points à la plate-forme MZ par communication série
J'ai essayé de résumer les points clés de la conception et du développement de gRPC
J'ai essayé de résoudre le problème de la séquence Tribonacci en Ruby, avec récurrence.
J'ai essayé d'utiliser pleinement le cœur du processeur avec Ruby
Après tout, je voulais prévisualiser le contenu de mysql avec Docker ...
J'ai essayé de résumer les méthodes utilisées
J'ai essayé d'utiliser Realm avec Swift UI
J'ai essayé de démarrer avec Web Assembly
J'ai essayé d'utiliser Scalar DL avec Docker
J'ai essayé d'utiliser OnlineConverter avec SpringBoot + JODConverter
J'ai essayé d'implémenter le modèle Iterator
J'ai essayé d'utiliser OpenCV avec Java + Tomcat
J'ai essayé de résumer l'API Stream