RSS

Dispose parallèle et efficace

Dans .NET, pour chaque classe utilisée en parallèle (par plusieurs tâches, ou « thread safe » pour les anglophones) et implémentant IDisposable, il faut péniblement implémenter une méthode « Dispose » fonctionnant en parallèle aussi.
Ici je propose une petite structure qui nous facilite la vie, et qui en plus a le mérite d’être efficace: rapide et petite.

Code de la structure:

internal struct DisposeParallèle
{
enum LibéréOuPas { PasEncoreSupprimé, DéjàSupprimé }

int déjàSuppriméOuPas;

internal void Dispose(Action MéthodeDeSuppression)
{
// "Interlocked" est bien plus rapide que "lock".

LibéréOuPas l = (LibéréOuPas)
System.Threading.Interlocked.CompareExchange(
ref this.déjàSuppriméOuPas,
(int)LibéréOuPas.DéjàSupprimé,
(int) LibéréOuPas.PasEncoreSupprimé);

// Ici, "déjàSuppriméOuPas" a toujours pour valeur DéjàSupprimé, et il n'en changera plus. 😉

if (l == LibéréOuPas.PasEncoreSupprimé)
MéthodeDeSuppression();
}
}

Remarques:

  1. On utilise « Interlocked », qui est bien plus rapide que « lock ».
    La preuve: http://www.drdobbs.com/windows/boosting-performance-with-atomic-operati/226900048
  2. On se contente de mémoriser un simple entier 32 bits, là où un « lock » aurait nécessité un objet et un booléen.
    J’aurais aimé ne mémoriser qu’un booléen, d’ailleurs, mais « Interlocked »
    ne propose pas de méthode « CompareExchange » sur un booléen.
  3. En tant que simple structure, on économise les ressources objets, et on n’a pas besoin d’appeler un constructeur.

Exemple d’utilisation:

internal class MaClasse : IDisposable
{
DisposeParallèle disposeParallèle;

void IDisposable.Dispose()
{
this.disposeParallèle.Dispose(this.suppression);
}

void suppression()
{
// votre code...
}

~MaClasse() // si nécessaire
{
((IDisposable)this).Dispose();
}
}

Ça reste assez simple à utiliser, avec un code bien découpé donc facile à maintenir.

 

Étiquettes : , , , , , , , ,

Dans Firefox, refaire apparaître l’annonce de mise à jour

Contexte

Depuis quelques années, Firefox propose une mise à jour
(semi-)automatique. Une petite fenêtre apparaît en bas à droite de
l’écran pour nous avertir qu’une nouvelle version du logiciel a été
publiée, et nous propose un lien facilitant cette mise à jour.

Malheureusement, ce message n’apparait qu’une seule fois, et reste
même très peu de temps présent sur l’écran.
Une fois cette occasion de cliquer partie, une mise à jour de Firefox
devient nettement plus difficile, obligeant en fait à télécharger
l’installeur à partir du site de Mozilla, à entrer le mot de passe
administrateur, etc.

Il nous faut donc un moyen de faire réapparaître la petite fenêtre
annonçant la mise à jour.

La solution

Une méthode assez simple consiste à modifier un paramètre dans Firefox:

  1. Dans la barre d’adresse de Firefox, entrez ce texte:
    about:config
    Puis validez.
  2. Confirmez l’accès à la zone des paramètres, si cela vous est
    demandé.
  3. En haut, dans la partie permettant d’effectuer des recherches
    dans les paramètres, entrez ce texte:
    app.update.lastUpdateTime.background-update-timer
  4. Faites un clic droit sur la seule ligne qui apparait dans la
    liste en dessous, qui porte le même nom,
    puis choisissez l’option Modifier.
  5. Remplacez le nombre par 0 (zéro).
  6. Redémarrez Firefox.
  7. Naviguez normalement avec Firefox.
    Au bout d’environ 1 minute, notre fameuse petite fenêtre indiquant la
    disponibilité d’une mise à jour apparait. À vous de cliquer assez vite
    sur le lien qu’elle contient !

Voila.

 
Poster un commentaire

Publié par le 2013-11-12 dans Astuces

 

Étiquettes : , ,

UniversalSerializer

[ Le code source se trouve dans l’article original en anglais et mis à jour, voir ici ]

Un sérialiseur universel pour .Net .

C’est quoi ?

UniversalSerializer est un sérialiseur pour .Net.
En d’autres termes, il transcode un arbre d’instances d’objets en un tableau d’octets, et vice versa.

En résumé

L’objectif de UniversalSerializer est d’être capable de sérialiser tout type sans effort.

  • Pas besoin d’ajouter des attributs ni des interfaces aux types (classes & structures).

  • Lorsqu’une instance de classe est référencée plusieurs fois, elle n’est sérialisée qu’une seule fois.

  • Les références circulaires sont autorisées.

  • Les mécanismes de sérialisation et de transcodage existant sont réutilisés.
    Actuellement: [Serializable], ISerializable, [ValueSerializer] et [TypeConverter].

  • Les types sans constructeur par défaut (sans paramètres) sont autorisés, si un constructeur paramétrique peut être exploité (c’est automatique).

  • Les classes ICollection non génériques peuvent être sérialisées si une méthode Add ou Insert peut être exploitée (c’est automatique).

Bien entendu, toutes les constructions ordinaires telles que les classes, structures, propriétés publiques, champs publics, énumérations, collections, dictionnaires, etc.. sont sérialisées par UniversalSerializer.

Lorsqu’un type n’est pas directement sérialisable, UniversalSerializer propose deux mécanismes:

  • ITypeContainers
    Nous pouvons englober le type (ou l’ensemble de types) qui pose problème dans une classe personnalisée qui gérera sa sérialisation et sa désérialisation.

  • Un ensemble de filtres
    Nous pouvons bloquer certains types, et demander au sérialiseur de stocker certains champs privés de la source.

Mon plus grand souhait est que les gens ajoutent de nouveaux Conteneurs et filtres dans le futur, et qu’ainsi peut-être un jour tous les types pourront être sérialisés.

Exemples d’utilisation

var data = new Window();
byte[] DataBytes = UniversalSerializer.Serialize(data);
var data2 = UniversalSerializer.Deserialize<Window>(DataBytes);

C’est aussi simple que ça !

Exemple pour WPF

Il existe une DLL spécialisée pour WPF, qui gère plus de types de WPF:

var data = new Window();
byte[] bytesData = UniversalSerializerWPF.SerializeWPF(data);
var data2 = UniversalSerializerWPF.DeserializeWPF<Window>(bytesData);

Exemple pour WinForms

Il existe une DLL spécialisée pour WinForms, qui gère plus de types de WinForms:

var data = new Form();
byte[] DataBytes = UniversalSerializerWinForm.SerializeWinForm(data);
var data2 = UniversalSerializerWinForm.DeserializeWinForm<Form>(DataBytes);

Pourquoi et comment est-ce fait ?

J’avais besoin d’un sérialiseur universel, capable de sérialiser n’importe quoi sans modification.

Mais les sérialiseurs existant posent des problèmes:

  • Certains nécessitent des attributs ou des interfaces spéciaux.
  • D’autres ne prennent pas en compte les champs.
  • Les références sont un vrai problème. En général, les instances sont dupliquées, et les références des objets désérialisés pointent vers différentes instances.
    Et les références circulaires ne sont jamais gérées.
  • Aucun ne peut gérer des types sans constructeur par défaut (sans paramètres), pour autant que je sache.
  • Certaines classes .Net particulières nécessitent plus d’attention mais sont scellées et donc ne peuvent pas être héritées avec des attributs de sérialisation.

Donc la solution nécessitait un ensemble de techniques et de mécanismes.
Nous allons passer en revue ces mécanismes dans les prochains chapitres.

Le code original

UniversalSerializer est construit à partir de fastBinaryJSON 1.3.7, de Mehdi Gholam, qui sérialise vers un format binaire inspiré par JSON.
UniversalSerializer y ajoute une certaine universalité en permettant de sérialiser n’importe quel type.
Notez que j’ai dû modifier fastBinaryJSON en profondeur, donc je ne peux plus me synchroniser avec le code source original de fastBinaryJSON. Par conséquent, UniversalSerializer n’est une extension de fastBinaryJSON et je ne maintiendrai pas de compatibilité dans le futur.

Sérialiser des classes sans constructeur par défaut

Dans .Net, les constructeurs de classes par défaut (sans paramètres) ne sont pas imposés. Mais presque tous les sérialiseurs en ont besoin.
Et des classes sans constructeur par défaut sont fréquentes dans la plateforme. Exemple: System.Windows.Controls.UIElementCollection.

La solution dans UniversalSerializer est de rechercher les autres constructeurs, et de trouver une correspondance entre leurs paramètres et des champs de la classe, même s’ils sont privés.

Par exemple, dans UIElementCollection, nous trouvons ce constructeur:

public UIElementCollection( UIElement visualParent, FrameworkElement logicalParent )

Et ces champs sont disponibles dans la même classe:

  • private readonly UIElement _visualParent;
  • private readonly FrameworkElement _logicalParent;

Les types sont les mêmes et leur nom très proches. Assez pour que UniversalSerializer essaie de créer une instance avec ces valeurs.
Et ça marche !

Sérialiser des classes ICollection non génériques

Pour d’obscures raisons, l’interface ICollection ne fournit pas de méthodes Add et Insert.
En d’autres termes, elle définit une collection en lecture-seule, au contraire de l’interface générique ICollection<T>.
Normalement, nous ne pourrions pas désérialiser une classe qui implémente ICollection mais pas ICollection<T>.

Heureusement, dans le monde réel, la quasi-totalité de ces classes possèdent une méthode Add ou Insert, au moins pour un usage interne.
UniversalSerializer détecte ces méthodes et les utilise pour désérialiser une instance de classe collection.

Réutiliser les mécanismes existant et les gérer à l’aide de ITypeContainers

Dans certains cas, il est plus efficace ou seulement possible d’utiliser les mécanismes de transcodage existant.

Lorsque j’ai essayé de sérialiser des contrôles WPF, j’ai découvert que:

  • [TypeConverter] permet de transcoder un objet vers des types facilement sérialisables (string, byte[ ], etc.).
    Exemple: System.Windows.Input.Cursor
  • [ValueSerializer] permet de transcoder à partir et vers un string.
    Exemple: System.Windows.Media.FontFamily
  • [Serializable] (et ISerializable) permet d’utiliser BinaryFormatter.
    Exemple: System.Uri

Si vous examinez FontFamily, vous vous apercevrez que la transcoder vers un string est bien plus facile que d’essayer de sauvegarder ses propriétés.
Et plus sûr, car modifier une propriété peut entraîner des conséquences imprévisibles sur des classes inconnues ou complexes.

Pour ces attributs, j’ai créé le mécanisme de ITypeContainer. Un conteneur remplace l’instance source par sa valeur transcodée, en général un string ou un byte[ ].
Un conteneur peut s’appliquer à un ensemble de types, par exemple tous les types affublés d’un certain attribut.

Des exemples et des détails suivent.

Les Filtres

Certains types nécessitent une gestion particulière, qui peut être faite par un ensemble de filtres.

Le filtre de validation de type

Ce filtre permet d’éviter à UniversalSerializer de sérialiser des types problématiques.

Par exemple, j’ai rencontré certaines classes qui utilisent System.IntPtr .
Sérialiser ce type mène droit à des problèmes vu que ses instances sont uniquement utilisées en interne dans les classes, même lorsqu’elles sont stockées dans des propriétés publiques.

Les filtres d’ajout de champs privés

Ce filtre ordonne au sérialiseur d’ajouter certains champs privés aux informations de sérialisation.

Par exemple, System.Windows.Controls.Panel a besoin de _uiElementCollection pour remplir sa propriété Children, car Children est en lecture-seule.
Grâce à ce filtre, la solution est simple. Et tous les types héritant de Panel, comme StackPanel, bénéficient du même filtre.

ForcedParametricConstructorTypes

Ce n’est pas un filtre mais une liste de types.
Lorsqu’un type est dans cette liste, UniversalSerializer ignore son constructeur par défaut (sans paramètres) et recherche un constructeur paramétrique.
Exemple: System.Windows.Forms.PropertyManager . Il est ici bien plus simple d’utiliser son constructeur paramétrique que d’écrire un ITypeContainer pour ce type.

Les références

Examinons ce code:

{ // Two references to the same object.
  var data = new TextBox[2];
  data[0] = new TextBox() { Text = "TextBox1" };
  data[1] = data[0]; // Same reference
  byte[] bytesData = UniversalSerializer.Serialize(data);
  var data2 = UniversalSerializer.Deserialize<TextBox[]>(bytesData);
  data2[0].Text = "New text"; // Affects the two references.
  bool sameReference = object.ReferenceEquals(data2[0], data2[1]);
}
  1. UniversalSerializer sérialise une seule instance du TextBox.
  2. Il désérialise ensuite seulement une seule instance de TextBox, et deux références pointant vers elle.
    Les preuves: sameReference est vrai, et Text est identique dans les deux références.

Des filtres et des ITypeContainer personnalisés

Étant donné que l’objectif majeur de UniversalSerializer est de permettre de sérialiser n’importe quel type, il est essentiel que nous partagions notre expérience.

Par conséquent, j’ai essayé de rendre la création de conteneurs et de filtres aussi facile que possible, pour vous permettre de les essayer dans tous les sens et de partager ensuite vos solutions.

Créer un ITypeContainer

Le but est de remplacer une instance problématique par une instance facile à sérialiser. Le plus souvent, le conteneur contiendra des types très simples (string, int, byte[ ], etc..).

Prenons un exemple:

/// <summary>
/// . No default (no-param) constructor.
/// . The only constructor has a parameter with no corresponding field.
/// . The field ATextBox has no public 'set' and is different type from constructor's parameter.
/// </summary>
public class MyStrangeClassNeedsACustomerContainer
{
    /// <summary>
    /// It is built from the constructor's parameter.
    /// Since its 'set' method is not public, it will not be serialized directly.
    /// </summary>
    public TextBox ATextBox { get; private set; }
    public MyStrangeClassNeedsACustomerContainer(int NumberAsTitle)
    {
        this.ATextBox = new TextBox() { Text = NumberAsTitle.ToString() };
    }
}

Comme noté dans son résumé, cette classe pose quelques problèmes au(x) sérialiseur(s).

Pour régler ce problème, nous créons un conteneur:

class ContainerForMyStrangeClass : UniversalSerializerLib.ITypeContainer
{
#region Here you add data to be serialized in place of the class instance
public int AnInteger; // We store the smallest, sufficient and necessary data.
#endregion Here you add data to be serialized in place of the class instance

public UniversalSerializerLib.ITypeContainer CreateNewContainer(object ContainedObject)
{
MyStrangeClassNeedsACustomerContainer sourceInstance = ContainedObject as MyStrangeClassNeedsACustomerContainer;
return new ContainerForMyStrangeClass() { AnInteger = int.Parse(sourceInstance.ATextBox.Text) };
}
public object Deserialize()
{
return new MyStrangeClassNeedsACustomerContainer(this.AnInteger);
}
public bool IsValidType(Type type)
{
return Tools.TypeIs(type, typeof(MyStrangeClassNeedsACustomerContainer));
}
public bool ApplyEvenIfThereIsANoParamConstructor
{
get { return false; }
}
public bool ApplyToStructures
{
get { return false; }
}
}

Un détail: toutes les méthodes se comportent comme des méthodes statiques (mais sans l’être officiellement), à l’exception de Deserialize().

Voyons-les plus en détail:

  • public int AnInteger

    Ça ne fait pas partie de l’interface ITypeContainer.
    C’est ici que nous stockerons les informations qui serviront plus tard à la désérialisation.

  • ITypeContainer CreateNewContainer(object ContainedObject)

    Utilisé durant la sérialisation.
    C’est un genre de constructeur pour une instance de ce conteneur. Le paramètre sera l’instance de la classe source à sérialiser.

  • object Deserialize()

    Utilisé durant la désérialisation.
    L’instance du conteneur produira une nouvelle instance, une copie de l’instance de la classe source, en utilisant notre champ AnInteger.

  • bool IsValidType(Type type)

    Utilisé durant la sérialisation.
    Renvoie vrai si le type hérite, ou est, du type source.
    C’est un filtre.
    Nous pouvons choisir d’accepter les types hérités ou non, d’accepter des types compatibles, etc..

  • bool ApplyEvenIfThereIsANoParamConstructor

    Utilisé durant la sérialisation.
    Renvoie vrai si ce conteneur s’applique aux types de classes possédant un constructeur par défaut (sans paramètres).
    Peut être utile avec des conteneurs très généraux.

  • bool ApplyToStructures

    Utilisé durant la sérialisation.
    Renvoie vrai si ce conteneur s’applique aux types structure, et pas seulement aux types classe.
    Peut être utile avec des conteneurs très généraux.

Les étapes sont:

  1. Le sérialiseur teste si le type source (MyStrangeClassNeedsACustomerContainer) est géré par ce conteneur.
    Notre classe de conteneur (ContainerForMyStrangeClass) réponds que oui, via IsValidType(), ApplyEvenIfThereIsANoParamConstructor et ApplyToStructures.
  2. Le sérialiseur construit une instance de notre conteneur, via CreateNewContainer().
    CreateNewContainer construits une instance et modifie son champ AnInteger.
  3. Le sérialiseur stocke (sérialise) ce conteneur à la place de l’instance source.
  4. Le sérialiseur retrouve (désérialise) l’instance du conteneur.
  5. Le désérialiseur appelle Deserialize() et obtient une copie de l’instance de la classe source.
    Deserialize() crée cette copie en utilisant son champ AnInteger.

Plus qu’a sérialiser:

/* This example needs a custom ITypeContainer.
Normally, this class can not be serialized (see details in its source).
But thanks to this container, we can serialize the class as a small data (an integer).
*/
var Params = new UniversalSerializerLib.CustomParameters();
Params.Containers = new UniversalSerializerLib.ITypeContainer[] {
new ContainerForMyStrangeClass()
};
var data = new MyStrangeClassNeedsACustomerContainer(123);
byte[] bytesData = UniversalSerializer.Serialize(data, Params);
var data2 = UniversalSerializer.Deserialize<MyStrangeClassNeedsACustomerContainer>(bytesData, Params);
bool ok = data2.ATextBox.Text == "123";

Comme vous pouvez le voir, cette implémentation est très facile.

La classe statique Tools nous offre de l’aide:

  • Type Tools.TypeIs(Type ObjectType, Type SearchedType)

    C’est l’équivalent du ‘is’ en C#, mais pour les Types.
    Par exemple, TypeIs((typeof(List<int>), typeof(List<>)) renvoie true.

  • Type DerivedType(Type ObjectType, Type SearchedType)

    Renvoie le type correspondant à SearchedType qui est hérité par OjectType.
    Par exemple, DerivedType(typeof(MyList), typeof(List<>)) renvoie typeof(List<int>) lorsque MyList est un

    MyList: List<int> { }.

  • FieldInfo FieldInfoFromName(Type t, string name)

    Renvoie le FiedInfo du champ nommé de ce type.
    Nous l’utiliserons dans le prochain chapitre.

Créer un ensemble de filtres

Notez que le mécanisme des filtres est complètement indépendant des ITypeContainers.
On peut les utiliser ensemble, ou séparément.

Prenons un exemple:

public class ThisClassNeedsFilters
{
public ShouldNotBeSerialized Useless;
private int Integer;
public string Value { get { return this.Integer.ToString(); } }
public ThisClassNeedsFilters()
{
}
public ThisClassNeedsFilters(int a)
{
this.Integer = a;
this.Useless = new ShouldNotBeSerialized();
}
}
public class ShouldNotBeSerialized
{
}

Cette classe (ThisClassNeedsFilters) pose quelques problèmes:

  • Elle contient un ShouldNotBeSerialized.
    Nous supposerons que la classe ShouldNotBeSerialized doit être évitée pour certaines raisons, je ne sais pas pourquoi, peut-être qu’elle est empoisonnée !
  • Le champ Integer n’est pas public et donc est ignoré par le(s) sérialiseur(s).
  • Même le nom du paramètre du constructeur est différent d’aucun champ ou propriété.
    De toute façon, le sérialiseur n’a pas besoin du constructeur, puisqu’il y a un constructeur par défaut.

Pour dépasser ces problèmes, nous écrivons un ensemble personnalisé de filtres:

/// <summary>
/// Tells the serializer to add some certain private fields to store the type.
/// </summary>
FieldInfo[] MyAdditionalPrivateFieldsAdder(Type t)
{
if (Tools.TypeIs(t, typeof(ThisClassNeedsFilters)))
return new FieldInfo[] { Tools.FieldInfoFromName(t, "Integer") };
return null;
}
/// <summary>
/// Returns 'false' if this type should not be serialized at all.
/// That will let the default value created by the constructor of its container class/structure.
/// </summary>
bool MyTypeSerializationValidator(Type t)
{
return ! Tools.TypeIs(t, typeof(ShouldNotBeSerialized));
}

Ils parlent d’eux-même:

  • FieldInfo[] MyAdditionalPrivateFieldsAdder(Type t)

    dit au sérialiseur d’ajouter un champ privé (Integer) à chaque instance source de ce type (ThisClassNeedsFilters).

  • bool MyTypeSerializationValidator(Type t)

    empêche le sérialiseur de stocker toute instance de ce type (ShouldNotBeSerialized).
    En conséquence, tout instance de ThisClassNeedsFilters ne fixera pas son champ Useless (il sera null après la désérialisation).

Maintenant on sérialise la classe:

/* This example needs custom filters.
Normally, this class can be serialized but with wrong fields.
Thanks to these filters, we can serialize the class appropriately.
*/
var Params = new UniversalSerializerLib.CustomParameters();
Params.FilterSets = new FilterSet[] {
new FilterSet() {
AdditionalPrivateFieldsAdder=MyAdditionalPrivateFieldsAdder,
TypeSerializationValidator=MyTypeSerializationValidator } };
var data = new ThisClassNeedsFilters(123);
byte[] bytesData = UniversalSerializer.Serialize(data, Params);
var data2 = UniversalSerializer.Deserialize<ThisClassNeedsFilters>(bytesData, Params);
bool ok = data2.Value == "123" && data2.Useless == null;

L’implémentation est même plus facile qu’avec ITypeContainer.

Le code source de UniversalSerializer

Toutes les sources sont en C#.

La solution a été créée avec Visual Studio 2012, mais elle doit être compatible avec VS 2010.
La compilation nécessite seulement .Net 4, aucune DLL tierce, le code source est complet.

Il y a 3 DLLs:

  • UniversalSerializer1.dll
    La DLL principale générale, avec peu de dépendances.
  • UniversalSerializerWPF1.dll
    Une DLL spécialisée pour WPF, qui gère plus de types de WPF.
  • UniversalSerializerWinForm1.dll
    Une DLL spécialisée pour WinForms, qui gère plus de types de WinForms.

De cette façon, votre application n’aura pas de dépendances inutiles.

Dans l’archive, vous trouverez ces solutions pour VS:

  • \UniversalSerializer Tester\UniversalSerializer Tester.sln
    Une application WPF avec de nombreux exemples.
  • \UniversalSerializerWinForm\UniversalSerializerWinForm.sln
    Une application WinForm avec quelques exemples.
  • \UniversalSerializerReleaseLibs\UniversalSerializerReleaseLibs.sln
    Cette solution génère la version Release des trois DLLs.

Quelques points important

  • Le processus de sérialisation/désérialisation  identifie les types par leur nom complet (grâce à type.AssemblyQualifiedName).
    Ce genre de nom dépend de la version de l’assemblage (assembly), alors faites attention aux problèmes de versions pour la plateforme et pour les DLLs !

  • Le format produit actuel est sujet à changements dans le futur.
    C’est pour cela que le nom de la DLL porte un numéro de version.
    Si vous sauvegardez le tableau d’octets dans un fichier, je vous suggère d’ajouter une information de version à votre arbre d’objets.

Le futur

Je voudrais améliorer certains aspects:

  • Ajouter de nouveaux ITypeContainers et filtres.
    Vous pouvez m’aider. Prévenez-moi lorsque des Types ne sont pas sérialisés correctement. Et merci de partager votre solution, telle que des conteneurs et des filtres.
  • Réduire la taille des données produites.
    J’ai besoin de créer un nouveau format binaire.
  • Accélérer le traitement.

Faciliter le travail des sérialiseurs

Cette expérience m’a enseigné quelles difficultés rencontrent les sérialiseurs avec certains types et comment on peut leur faciliter le travail lorsque nous créons une classe.

  1. Écrivez un constructeur par défaut (sans paramètres) lorsque c’est possible.
    Le sérialiseur reconstruira l’instance à partir des champs et des propriétés.
  2. Si vous ne pouvez pas écrire un constructeur par défaut (sans paramètres), écrivez un constructeur paramétrique dont les paramètres correspondent à des champs privés.
    Chaque paramètre doit être du même type que le champ, et leurs noms doivent être semblables. En fait les noms peuvent différer légèrement: Param -> param ou _param ou encore _Param.
  3. Implémentez un ValueSerializerAttribute ou un TypeConverterAttribute lorsqu’une instance peut être construite à partir de quelque chose d’aussi simple qu’un string.
    En particulier lorsque votre classe contient de nombreux champs et propriétés d’optimisation publics.
    Par exemple, FontFamily peut être construit à partir d’un simple string; peu importe s’il contient bien d’autres informations, elles peuvent toutes être retrouvées à partir de ce simple string.
    Je ne sais pas si beaucoup de sérialiseurs prennent ces attributs en compte, mais au moins UniversalSerializer le fait.
  4. Tous les champs et les propriétés d’optimisation devraient être privés, lorsque c’est possible. Voir plus haut.
  5. Lorsque vous créez une collection d’objets, implémentez IList.
      Parce que ICollection ne permet pas d’ajouter des éléments à une collection.
    Lorsque vous créez une collection générique, implémentez ICollection<>.
      Parce que IEnumerable<> ne permet pas d’ajouter des éléments à une collection.

Remerciements

Je remercie Mehdi Gholam pour son fastBinaryJSON.
J’ai appris des choses intéressantes en lisant son code.
Merci pour avoir partagé cela.

 
Poster un commentaire

Publié par le 2013-07-13 dans .Net, Programmation

 

Étiquettes : , , ,

Le respect du client… de l’API

Les API changent, évoluent, s’améliorent, et ne sont pas toujours compatibles entre leurs versions.

C’est ce qui fait qu’un logiciel vieux de deux ans ne fonctionne parfois plus correctement, souvent après une mise à jour du système d’exploitation.
Le problème est général à la programmation, donc il concerne tous les systèmes d’exploitation.

Au final, ce qui cause ces problèmes, particulièrement énervants pour l’utilisateur final, c’est avant-tout un choix.

Choix de la compatibilité entre versions d’une API

[ Ici, je parle de versions majeures des API, et non des mises à jour mineures ]

Pour clarifier un peu le problème, voici une liste des différents choix que j’ai pu rencontrer jusqu’à présent.

Aucune compatibilité

Ça peut paraître absurde à beaucoup, mais certains programmeurs ou architectes font le choix de ne tout simplement pas permettre à un logiciel prévu pour une version A d’une API de fonctionner avec la version B de la même API.

Schématiquement, ça ressemble à ceci:

Ici les différentes versions ne proposent aucune compatibilité.
C’est évidemment ce qui demande le moins de travail aux programmeurs.
Par contre, un logiciel basé sur une API A devra être livré avec la même version A de la bibliothèque de fonctions, ce qui peut représenter une grosse taille, du temps de téléchargement et des incompatibilités à l’exécution.

Redirection vers la version immédiatement suivante

Note: C’est un concept différent de la compatibilité descendante.

Schématiquement:

Ici, lorsqu’on crée une nouvelle version (B) de l’API, la 1ère version (A) est réécrite pour renvoyer vers la version suivante (B). Et ainsi de suite.

L’intérêt de cette technique, c’est qu’un logiciel écrit pour une très vieille version de l’API (A) peut utiliser l’API la plus récente (E), sous forme de sa bibliothèque de fonctions.
Cela évite le problème de l’Enfer des bibliothèques, c’est à dire essentiellement qu’on n’a pas besoin de livrer chaque logiciel avec ses bibliothèques associées.

Avantage supplémentaire: les anciennes API étant maintenues, par le biais des renvois vers des versions plus récentes, on trouve moins de bogues.

Du point de vue du travail, cela oblige, à chaque nouvelle version d’un API, de revoir la précédente pour remplacer ses fonctions obsolètes par des renvois vers la nouvelle.
Ça peut sembler représenter beaucoup de travail, mais en pratique ce n’est pas tellement le cas, car:

  1. En général une faible partie de l’API change d’une version à une autre.
    Les plus gros changements ont lieu dans l’implémentation des fonctions, ce qui en soi ne change rien à l’API.
  2. Il est classique que les fonctions et les concepts d’une nouvelle version d’une API englobent et surpassent ceux de l’ancienne version.
    Par exemple, une fonction pouvant calculer le sinus d’un nombre flottant sera étendue pour pouvoir calculer aussi le sinus d’un nombre imaginaire (pourquoi pas ?).
    Dans un tel cas, l’ancienne fonction peut tout simplement renvoyer vers la nouvelle, avec des paramètres par défaut. Car qui peut le plus peut le moins.

Ce système, bien que limité à des paires de versions, permet tout de même à la plus ancienne version (A) de renvoyer, par bonds successifs, vers la plus récente (E). Pour une somme de travail acceptable, ça reste d’un intérêt non négligeable.

Redirection vers la dernière version

Schématiquement:

L’avantage de ce principe, c’est que toutes les anciennes versions renvoient vers la dernière, ce qui les maintient le plus à jour possible, évitant les vieux bogues et utilisant les toutes dernières techniques.

Par exemple, si c’est l’API d’un système de fenêtrage, alors même une très vieille application utilisera la toute dernière bibliothèque et donc elle aura un affichage dernier cri, tout-à-fait compatible avec les applications les plus récentes, et elle sera bien intégrée dans le système.

L’inconvénient est bien sûr que le travail est de plus en long à chaque version.
De plus il faut avoir des programmeurs connaissant bien les plus anciennes versions de l’API, ce qui est de plus en plus rare au fil du temps car les plus jeunes gens sont formés sur les API les plus récentes.

Par contre, du point de vue de l’utilisateur final, c’est la meilleure solution.

Réutilisation d’une plus ancienne version

Schématiquement:

Ici, une nouvelle version renvoie vers une plus ancienne.
Étrangement, ce concept rétrograde est très présent dans les systèmes d’exploitation.

Il est d’ailleurs systématiquement utilisé dans cet autre concept: les plateformes.

Exemple de Windows (8)

Ce choix paraît justifié par la grande différence qui existe entre l’API de la nouvelle plateforme et celle de l’ancienne.
Toutefois, je ne peux pas m’empêcher de penser qu’il serait bien plus logique de choisir un des autres concepts de compatibilité.

Dans ce dernier exemple, DotNet aurait pu être créé à partir de zéro et les parties obsolètes de Win32 réécrites pour n’être plus qu’un renvoi vers l’API la plus récente.
Idem pour WinRT.
Bien entendu, il est clair que cela aurait demandé bien plus de travail et causé bien plus de difficultés.
Il aurait fallu réfléchir longuement et mettre au point de vrais nouveaux concepts, les expérimenter tout aussi longuement. Et ensuite il aurait fallu réécrire toute la partie de Win32 devant renvoyer vers WinRT (par exemple).

Ceci dit, le résultat aurait été d’un grand avantage: Windows 8 aurait été un système homogène, au lieu de proposer deux types d’interface graphique différents et séparés.

On peut aussi remarquer que pour Windows 8 Microsoft a dû réécrire de nombreux logiciels et panneaux de configuration pour WinRT, ce qui représente également du travail. D’un certaine façon, ils ont économisé du travail avec ce choix de sur-plateforme, mais en ont gaspillé avec la réécriture des applications.

Toutefois, il faut aussi prendre en compte le bénéfique renouvellement des applications. Une application réécrite l’est en tentant compte de nouveaux concepts et des dernières techniques (par nécessairement liés à la plateforme).

Conclusion

Il me semble clair qu’il n’existe aucune méthode parfaite, et qu’on doit faire des choix difficiles.

Toutefois si on a une vision d’une informatique pérenne, alors il existe au moins une façon de faciliter l’évolution d’une API: la créer la plus simple et de plus haut niveau d’abstraction que possible, pour éviter que ses fonctions et ses concepts ne soient vite périmés.

Il me semble nécessaire d’éviter les fonctions trop spécialisées.
Nous avons une culture de l’optimisation qui nous vient des temps anciens lorsque les ordinateurs étaient si peu puissants qu’il fallait compter chaque octet et chaque temps d’horloge.
Il faut donc désormais penser différemment, plus tournés vers l’avenir et donc vers des solutions plus universelles et plus pérennes, au moins pour les API.
Cela ne nous empêchera pas d’optimiser l’implémentation des fonctions. Après tout, l’API n’est qu’une façade, une porte d’accès.

 
Poster un commentaire

Publié par le 2012-11-06 dans Programmation

 

Étiquettes : , , , , ,

HTML 5 n’est pas une interface riche !

HTML 5 ne permet pas plus de créer des « interfaces riches » que des applications.

Depuis quelques temps, l’industrie informatique semble devenue follement attirées par HTML 5. On lit partout qu’il va permettre de créer de l’ « Internet riche » multi-plateformes. On en arriverait presque à croire qu’on va (enfin) pouvoir créer des applications portables, qu’on ne programmerait qu’en une seule version et qui tournerait non seulement sur tous les systèmes d’exploitation mais tout aussi bien dans les navigateurs Web.

Et chaque entreprise y va de son discours montrant à quel point elle se sent impliquée dans ce merveilleux mouvement, et comment ses produits vont même contribuer à son épanouissement.
Tout le monde il est beau, tout le monde il est gentil, sympa, fantastique. Quel jubilation !
Dommage que ça ne protège pas la couche d’ozone tant qu’on y est. 😉

Microsoft, lorsqu’il nous a montré les premières images de Windows 8, nous a abreuvé d’explications concernant sa nouvelle stratégie: à l’entendre, il misait dorénavant à fond sur l’HTML 5.
Adobe, lui aussi, nous parle de sa nouvelle stratégie de transformation vers l’HTML 5. Il est même prêt à abandonner son produit phare dans le domaine: Flash, et a publié un outil permettant de transformer les anciennes applications flash en HTML 5.
Apple nous dit pour sa part: « Apple ne supporte pas Flash parce qu’il est trop bogué. Chaque fois qu’un Mac plante, le plus souvent c’est à cause de Flash. Personne n’utilisera plus Flash. Le Monde est en train de se tourner vers HTML 5. » (Apple Town Hall Meeting, fin Janvier 2010).
Il semble qu’aucun éditeur de logiciels ne soit avare de belles promesses concernant l’HTML 5.

Seulement voila, il y a un hic:

HTML 5 n’est pas, et ne sera jamais, une plateforme d’applications

Les belles annonces des différents acteurs de l’informatique ont peu de rapport avec la réalité, du moins en substance.

Pour comprendre, il faut clarifier un peu les choses, c’est à dire séparer les faits techniques bruts et les discours commerciaux aussi grandiloquents qu’hypocrites et trompeurs.

Qu’est ce que le « Web riche » ?

Appelé aussi « Internet riche« .
Cette expression plutôt floue, en fait aussi mal définie que d’autres expressions commercialo-pseudo-techniques comme « Internet 2.0 », indique surtout une (relativement) nouvelle utilisation que l’on fait du Web.
[ Par Web, je me place du côté client, avec surtout HTML et Javascript. ]

Au fil du temps, le Web s’est un peu divisé en deux types de fonctionnements:

  1. Historiquement, il y a d’abord eu l’utilisation de documents textes, avec quelques ajouts simples comme des images, du son ou des vidéos.
  2. Puis petit-à-petit les sites Internet ont aussi voulu présenter et utiliser les informations d’une façon qui ressemble plus aux logiciels, notamment avec un contenu mis à jour sans changer de page.

Le problème pour passer d’un mode à l’autre était que les pages Web ne sont à la base que des documents textes statiques.
Ils sont comparables aux documents produits par Word ou par LibreOffice.

Malgré une légère évolution vers le « dynamisme » (les pages peuvent modifier leur apparence elles-mêmes), il restait difficile de faire correspondre les habitudes des utilisateurs, basées sur les interfaces graphiques et les applications, à la réalité textuelle du Web.

Imaginez que vous utilisez une messagerie en ligne pour lire votre courrier, et qu’à chaque arrivée de courrier la page se recharge entièrement, vous faisant perdre ce que vous étiez en train de faire.
Ça n’est évidemment pas pratique à utiliser, et il semble bien plus logique de réutiliser les principes classiques des applications et donc des interfaces graphiques. Au moins, une vraie application est construite autour de l’information qu’elle manipule (le courrier dans cet exemple), et non autour d’un texte qui changerait de forme selon l’arrivée du courrier.

Alors on a progressivement utilisé le Web d’une façon pour laquelle il n’était pas fait, tentant de passer d’un document texte à une interface logicielle et graphique.
On a d’abord ajouté un langage de programmation, Javascript, puis quelques années plus tard on a écrit des bibliothèques de fonctions pour permettre aux pages de mettre à jour leur contenu sans devoir être rechargées, comme Ajax.

À ce stade, on a donc des pages Web partiellement dynamiques, c’est à dire dont le contenu peut se mettre à jour en fonction des évènements.
Ceci dit, on est très loin d’avoir l’équivalent d’une interface graphique d’application.

Qu’est-ce qu’une application, une interface graphique, et une plateforme d’applications  ?

Une application avec interface graphique, c’est tout simplement l’apparence des logiciels que vous utilisez tous les jours sur votre ordinateur, comme votre éditeur de textes, votre navigateur Internet, ou votre lecteur audio. Bref c’est la façon d’utiliser les ordinateurs depuis le milieu des années 1980 (pour le grand public, et depuis les années 1970 pour quelques universitaires chanceux).

Ce qui différencie les interfaces graphiques des précédentes interfaces textuelles, c’est:

  1. Tout se manipule à la souris, grâce à des contrôles graphiques réutilisables.
    Chaque fenêtre est en fait constituée de ces contrôles: menus, boutons, listes, barres de défilement, zones d’édition de texte, arbres, etc..
  2. Les applications sont homogènes.
    Toutes sont dans des fenêtres, s’utilisent de la même façon, et sont construites à partir des mêmes contrôles graphiques de base.
    Lorsqu’on sait en utiliser une, on sait toutes les utiliser.
  3. On peut facilement échanger des informations entre les applications.
    Typiquement: le copier-coller. Et aussi le glisser-déplacer.

Voici un exemple des contrôles graphiques principaux dans la plateforme WPF:

Bien entendu, il existe bien plus de contrôles disponibles, pour afficher et modifier un arbre d’informations (comme la liste de répertoires dans l’explorateur de fichiers par exemple), ajouter des barres d’outils, des graphiques en courbes, des objets en 3D, etc.

Maintenant, ce que j’appelle plateforme d’applications avec interface graphique, c’est tout simplement l’environnement qui permet de lancer des applications, de les utiliser.
J’utilise ce terme général de plateforme, car elles se présentent sous plusieurs formes, et font plus ou moins partie intégrante des systèmes d’exploitation.

Dans Windows, l’ensemble GDI, User et Explorer forment en quelque sorte la plateforme graphique de base originale.
Mais par-dessus celle-ci, on trouve aussi des (sur-) plateformes graphiques, comme WinForms, WPF, Metro, voire Qt, etc..

Dans Linux, on divise traditionnellement ça entre un système de fenêtrage (ex: X Window), un gestionnaire de fenêtres (ex: Metacity) et un environnement graphique (ex: Gnome, KDE), et il en existe de nombreuses variantes.

En quoi HTML 5 n’est-il pas une plateforme d’applications ?

On a vu qu’une telle plateforme obéit à plusieurs règles générales assez simples.
La première repose sur des contrôles graphiques, qui doivent être assez nombreux pour servir à construire tout type d’application.

Bien trop peu de contrôles graphiques

Voyons comment créer une application avec HTML (5), par exemple un gestionnaire de fichiers tel que l’Explorateur de fichiers de Windows.
On crée un document, dont la forme générale sera celle d’une telle application: on voudrait placer:

  • en haut, une zone d’édition de texte pour entrer manuellement le nom d’un répertoire,
  • à gauche un contrôle graphique en arbre pour y placer les répertoires,
  • à droite un contrôle graphique de liste pour y placer les fichiers du répertoire sélectionné dans l’arbre,
  • tout en haut, un menu pour présenter des options plus détaillées, ou un ruban d’icônes si vous préférez ça.

Petit problème, HTML (5) ne dispose que de quelques rares contrôles graphiques, qui sont en fait adaptés à l’envoi de formulaires d’informations:

Exemple en HTML 4:
On a ici des zones d’édition de texte, une boîte à choix et des boutons.
C’est maigre.


Exemple en HTML 5:
Ici on trouve en plus une jauge, un éditeur d’heure et un choix de date par calendrier.
Ça reste maigre !

Comme vous le voyez, c’est très rudimentaire, et surtout les contrôles ont été choisis pour créer des formulaires, et non des applications.

Résultat, pour construire notre application en HTML, il nous manque donc tous les contrôles le plus évolués: menu, arbre, liste, ruban, pour ne mentionner que ceux que j’ai listé. En pratique, il en manque encore bien d’autres, y compris certains essentiels.

Des sur-plateformes pour le Web

Il existe des bibliothèques de fonctions qui sont écrites en Javascript/HTML, qui s’exécute donc entièrement dans le navigateur Internet, et qui tentent de constituer une plateforme avec interface graphique.
Par exemple, qooxdoo.
Il s’agit essentiellement de contrôles graphiques comblant les manques d’HTML, comme des arbres, des listes, des tableaux de données, des menus, des barres d’outils, etc.

C’est une excellente chose pour les programmeurs qui ont besoin de créer quelque chose d’approchant une application, et qui sont restreints au Javascript/HTML pour certaines raisons propres à leur projet.

Problèmes:

  • C’est très lent.
    Ce qui pose problème dans les téléphones portables et dans les tablettes de faible puissance (type iPad), par exemple.
  • Les applications ne communiquent pratiquement pas entre elles, ni avec celles du système d’exploitation.
    Juste un petit copier-coller de texte brut.
  • Aucune homogénéité entre les applications.
    Chaque sur-plateforme a sa propre apparence pour les contrôles graphiques.
    Plus grave encore: il est facile, et même encouragé, d’utiliser des thèmes graphiques personnalisés pour chaque application, ce qui les rend d’autant dissemblables.
  • Aucune homogénéité avec le système d’exploitation.
    L’apparence et la façon d’utiliser l’application seront assez différentes, perturbant ainsi l’utilisateur final.
    La raison est que les contrôles graphiques ne sont pas ceux du système.
  • Ça reste difficile à programmer.
    Javascript et Ajax sont nécessaires, et ils sont loin d’être aisés.
    Ils sont bien moins adaptés que C++ ou C#. Et je ne parle même pas de les comparer avec des techniques plus évoluées comme les liaisons automatiques sous WPF, par exemple.

De vraies plateformes d’application pour le Web

Il existe depuis assez longtemps de telles plateformes, complètes et performantes.
Ce sont notamment Java, Silverlight, et Flex/Flash.

À la différence des plateformes se plaçant au-dessus de Javascript/HTML, le code est exécuté de façon quasi-native, utilisant ainsi tout le potentiel du microprocesseur et de la machine en général.

Petit comparatif:

Javascript n’a pas directement accès aux fonctions du système d’exploitation.
D’où une énorme limitation de ses possibilités et une vitesse d’exécution très faible.
Java, Silverlight et Flex/Flash ont, eux, directement accès aux fonctions du système d’exploitation.
D’où une richesse et une vitesse d’exécution proches d’une application native.

On voit sur le graphique que ce type de plateforme fait le lien entre l’application Web et le système d’exploitation, sans passer par le « filtre » de JS/HTML/DOM.

Ce type de plateforme propose une interface graphique complète et un ou plusieurs langages de programmation performants (seul Flex/Flash ne propose vraiment qu’un langage).

Problème, pour la majorité, leur éditeur est en train de les abandonner au profit d’HTML 5 (ou Metro/WinRT pour Microsoft).
Seul Java reste activement développé.

Autre défaut, d’une façon générale, elles n’utilisent pas les contrôles graphiques natifs, c’est à dire qu’elles dessinent leurs propres contrôles, en essayant toutefois de ressembler à ceux du système d’exploitation.

Tout de même, ces plateformes sont bien plus évoluées que les dizaines d’éphémères sur-plateformes qui se placent au-dessus de Javascript/HTML.

En les abandonnant, leurs éditeurs condamnent les programmeurs, et toute l’industrie intéressée, à choisir entre la très mauvaise solution du Javascript/HTML et la encore plus mauvaise solution des petites sur-plateformes sur Javascript.

Le cas de Flex/Flash et de son transcodeur

Depuis un an ou deux, Adobe, son éditeur, semble vouloir abandonner Flex.
Pourtant Flex semblait une évolution intéressante de Flash, proposant enfin une plateforme d’applications.

Mais devant le succès médiocre de cette plateforme, Adobe préfère abandonner à la fois Flash et Flex, au profit d’HTML et parait même vouloir refaire sa vie avec les services aux utilisateurs finaux, peut-être inspiré par les bénéfices de Google, Facebook et autres.

Il est vrai que Flex avait pour (gros) défaut de ne proposer que le langage ActionScript aux développeurs, très proche du très peu professionnel Javascript.

Adobe a même publié un outil permettant aux infortunés programmeurs Flash de transformer leur oeuvres en HTML 5.
Petit problème, dont on parle peu, ce transcodeur ne permet pas de transformer des applications Flex à contrôles graphiques en Javascript/HTML. À vrai dire, comment l’aurait-il pu ?
Ce détail me semble hautement révélateur des limites d’HTML.
Si HTML avait eu une collection de contrôles graphiques, ce transcodeur aurait pu les utiliser. Eh bien non.

Le cas de Metro/WinRT

Metro/WinRT est la nouvelle plateforme de Microsoft, apparu avec Windows 8, et disponible uniquement dans ce système.
Elle peut aussi être utilisée comme plateforme Web, pour lancer une application à partir d’Internet Explorer.

Comme je l’ai dit en introduction, j’ai personnellement ressenti l’annonce par Microsoft de son revirement vers HTML 5 comme une hypocrisie, et même un leurre, lorsque comme tout le monde j’ai pu constater qu’en pratique HTML 5 ne leur sert que de fine peau au-dessus de leur nouvelle plateforme: Metro/WinRT.

Je dirais même que cette peau est comme un camouflage, ou un déguisement.
En pratique, loin d’être portable, une application développée pour Metro/WinRT, même s’exécutant dans HTML 5, ne fonctionnera que dans Internet Explorer et uniquement sous Windows 8.
En fait je soupçonne leur discours sur HTML 5 de n’être qu’un appel du pied aux programmeurs Web (qui pratiquent en général le HTML avec Javascript, et le PHP côté serveur). Dans ce but, on peut apparemment programmer une application WinRT en Javascript.
Je ne pourrai toutefois pas témoigner de cela, je n’userai pas mes nerfs à essayer de programmer avec ce langage conceptuellement nullissime de Javascript.

Les plateformes, ou systèmes d’exploitation, d’applications Web

Comment fonctionnent-ils ?

Dans le cas de WebOS, développé par Hewlett-Packard, on a un système d’exploitation complet destiné aux téléphones portables, et une plateforme d’applications qui est une sorte de navigateur Web. Les applications sont donc développées en Javascript dans un environnement HTML auquel s’ajoutent des bibliothèques du système d’exploitation.

Un échec prévisible déjà annoncé

Ce n’est pas de chance, ce système a déjà été abandonné, avant même d’avoir été intégré dans une machine.
Et cela après des années de développement, avec une équipe de plus de 500 personnes, et un coût probablement bien supérieur à un milliard d’Euros.
Selon divers avis internes, il semble qu’HP se soit finalement rendu à l’évidence: les applications développées en JS/HTML sont beaucoup plus lentes que leurs homologues en C++/Contrôles natifs. Étant donné que tout leur système était basé sur ce choix de JS/HTML, il ne leur restait plus qu’à l’abandonner.

Car déjà sur un ordinateur de bureau, les applications écrites en JS/HTML se traînent, alors je ne doute pas qu’elles devaient être carrément insupportables sur des téléphones dotés de processeurs plusieurs fois moins puissants.

Cet échec est à mon sens l’annonciateur du futur échec de ce mouvement de promotion des applications en HTML 5.

Un futur en forme de passé

Si les acteurs majeurs de l’informatique passent réellement à l’HTML 5 comme simili-plateforme d’applications, on va inévitablement se retrouver avec la situation que l’on connaissait à l’ère des interfaces textuelles comme MS-DOS ou Linux en mode console:

  1. Aucune homogénéité dans l’apparence et dans la façon d’utiliser les applications.
    Chacune étant basée sur son propre choix de sur-plateforme et de contrôles graphiques propriétaires.
  2. Une grande difficulté pour échanger des informations entre les applications.
    Un bête copier-coller de texte brut marchera, alors qu’il sera impossible de coller des informations plus complexes telles que des documents.
    Le glisser-déplacer ne fonctionnera pas, car il n’existe aucun mécanisme standard de ce genre en HTML, surtout pas avec des informations plus complexes que du texte.
  3. La création d’applications est difficile, lente et produit d’innombrables bogues.
    Autant dire que JS/HTML a la palme de la pire productivité, et l’application résultante a la palme de la pire qualité de service.
  4. La sécurité des application est très faible, voire inexistante.
    Hors le Web est le milieu le plus dangereux qui existe pour une application.
    Multipliez les facteurs aggravants, vous obtenez une situation explosive.

Et à ces défauts s’ajoutent les défauts propres au Javascript:

  • Sécurité générale du langage faible.
    C’est notamment dû au typage dynamique (en clair il n’existe pas de contrôle de typage).
    Ouvrez la console d’erreurs de votre navigateur Internet et baladez-vous dans plusieurs sites Internet, vous aurez la joie de voir qu’ils sont bourrés de bogues. À se demander comment ils peuvent encore afficher quelque chose.
  • Lenteur patente.
    Mettez à part les querelles et les débats sur les vitesses d’execution des divers langages de programmation, et contentez-vous de regarder la vitesse d’exécution d’une grosse simili-application en Javascript, vous aurez l’impression de revenir 10 ans en arrière (voire plus encore).

Globalement, tout ceci nous ramène 30 ans en arrière, et les premiers à en payer le prix, ce seront les utilisateur finaux, monsieur et madame tout-le-monde.
Ensuite suivra l’industrie du logiciel, par retour de bâton, puis toute l’industrie de l’informatique.
Qu’est-ce-qu’on va se marrer !

Ma conclusion

Si l’industrie du logiciel ne veut pas foncer tout droit vers un mur, elle doit impérativement cesser d’écouter les sirènes du HTML 5.

D’un point de vue technique, il me parait incontestable qu’HTML ne sera jamais autre chose qu’un format de document textuel.

D’ailleurs, lorsque Mozilla a voulu écrire une plate-forme au-dessus des systèmes d’exploitation, qui lui permettrait de n’écrire qu’une seule version de ses logiciels (dont FireFox) elle a créé un langage de description d’interface graphique, appelé XUL. Il est remarquable que la structure de ce langage n’ait rien de commun avec HTML.
La leçon à en tirer, c’est qu’HTML restera définitivement un simple langage décrivant des documents textes, et rien d’autre. Vouloir utiliser HTML pour créer des applications, c’est comme vouloir éplucher des patates avec un tournevis: on peut y arriver, mais il faudrait être idiot pour essayer.

Laissons HTML aux pages web, et encourageons les autres plateformes pour nos chères applications.

 
Poster un commentaire

Publié par le 2012-10-21 dans Programmation

 

Étiquettes : , , , , ,

La programmation des jeux dans les années 80

Au tout début de l’informatique grand public, les machines étaient si peu puissantes que la façon même de les programmer était différente d’aujourd’hui.
Il fallait se donner du mal pour n’obtenir qu’un résultat juste correct, l’environnement de programmation était assez rudimentaire.

Toutefois, ces difficultés allaient de pair avec l’enthousiasme d’apprendre un domaine tout nouveau et largement mystérieux pour le grand public.
Une des conséquences d’avoir appris à programmer à cette époque, outre l’entraînement à la persévérance😉 , c’est d’avoir finalement touché à toute la chaîne de l’informatique. Car pour obtenir un logiciel qui fonctionne normalement sur ces machines, il fallait tirer le maximum de leurs rares Mhz (oui, mega hertz, pas giga hertz !) et de leur mémoire d’oiseau amnésique. Il fallait donc les connaître dans leurs moindres détails.

Plutôt que de parler en général, je vais vous montrer en pratique à quoi cela ressemblait, en partie au travers de mon expérience personnelle et de quelques humbles œuvrettes de jeunesse.

Le ZX

Pour ma part, tout a démarré avec un précurseur de l’informatique accessible à tous, j’ai nommé le Sainclair ZX-81:

En résumé:
  • Pas cher (1000 FF à sa sortie, je crois, donc très accessible).
  • Enregistre sur des cassettes audio (il faut y brancher un magnétophone).
  • Mémoire ultra-faible même pour l’époque: 1 Ko !
  • Clavier de type industriel: une membrane avec des contacts dessous, comme certaines machines en plein-air.
  • Processeur 8 bits à 3,25 Mhz, que j’évaluerais à environ 0,15 Mips, le Z80.
  • Pas de mode graphique, tout est en texte.
  • Langage Basic intégré, assez simpliste.
  • Pas de son.

Détails sur Wikipedia.

Cette machine était construite pour que tout le monde puisse découvrir ce qu’est un ordinateur sans se ruiner.
Malheureusement, sa conception très bas-de-gamme le rendait presque inutilisable, sa faible puissance permettait à peine aux logiciels de tenir dans la mémoire et le manque de fiabilité de ses sauvegardes sur cassette audio nous faisait souvent perdre les programmes péniblement entrés à la main.

Il a toutefois servi à beaucoup de gens à se mettre à la programmation, car c’est bien ce qui était le plus passionnant à cette époque: créer quelque chose à partir de ces machines.

Comme toutes les machines 8 bits, il disposait d’un interpréteur Basic en ROM. D’ailleurs le système d’exploitation entier était en ROM, dans 8 petits Ko.

C’est une première chose étonnante vu d’aujourd’hui: comment un interpréteur Basic, en plus d’une gestion de fichiers sur cassette et de quelques autres ressources, pouvait tenir dans 8 minuscules Ko ?

La réponse est claire pour cette époque: tout le code de la ROM était écrit en Assembleur.
Pour les jeunes programmeurs, il n’est pas évident de savoir à quoi ressemble un code assembleur, puisqu’ aujourd’hui seuls quelques domaines particuliers nécessitent ce type de programmation.

Mais n’oublions pas que, par-dessus tout, la nécessité rend ingénieux. La capacité était faible, les gens ont trouvé des solutions pour en tirer partie malgré la difficulté.

Le code assembleur, dit code machine, en fait le jeu d’instructions du microprocesseur

Voici un extrait d’un code source, une partie de la routine de test de la RAM:

;; RAM-CHECK
L03CB: LD H,B ;
LD L,C ;
LD A,$3F ;

;; RAM-FILL
L03CF: LD (HL),$02 ;
DEC HL ;
CP H ;
JR NZ,L03CF ; to RAM-FILL

Vous trouverez le désassemblage complet de la ROM ici.
Malheureusement, ce genre de code est difficile à lire, c’est à dire à comprendre pour un humain.
Et encore, il faut imaginer un listing de 10000 lignes ainsi, on a vite fait de s’y perdre. Tout dépend alors de la qualité de la documentation.
Ce dernier code est à peu près équivalent à ce code C#:

void EffaceÉcran(byte* PFinÉcran)
{
// Remplissage:
byte* pécran = PFinÉcran;
while((pécran & 0xFF00) != 0)
*pécran-- = 2;
}

Vous pouvez voir que c’est déjà nettement plus lisible, surtout si vous savez programmer dans un langage proche syntaxiquement du C++. Et encore, on n’écrirait pas les choses ainsi en C#, on utiliserait un foreach.

Les pointeurs

À remarquer que mon transcodage en C# utilise un pointeur, déconseillé dans les langages modernes à cause des risques de dépassement. Ce pointeur reflète directement le registre HL du Z80 dans le code en assembleur.

On voit ici la raison d’être des pointeurs dans les langages comme le C++ : se calquer sur les instructions en code microprocesseur (ce qu’on appelle l’Assembleur, par un abus de langage, ou encore le code machine). On comprend bien, là, que le langage C est entièrement conçu pour coller au plus près des possibilités des microprocesseurs, ce qui le rend tout aussi rapide à l’exécution que dangereux.

L’inefficacité de la compilation C

Si je compile mon code C# (en fait une version en C K&R), j’obtiens ce long code machine (pour microprocesseur Z80):

 org &4000
nolist

jp main

main:
push bc
L1:
pop bc
ret
EffaceEcran:
push bc
ld hl,65536
add hl,sp
push hl
ld hl,6
add hl,sp
call Lgint
pop de
call Lpint
L3:
ld hl,65536
add hl,sp
call Lgint
push hl
ld hl,65280
pop de
call Land
push hl
ld hl,0
pop de
call Lne
ld a,h
or l
jp z,L4
ld hl,65536
add hl,sp
push hl
call Lgint
dec hl
pop de
call Lpint
inc hl
push hl
ld hl,2
pop de
call Lpchar
jp L3
L4:
L2:
pop bc
ret

; DATA

; CRT
Lpint:
ld a,l
ld (de),a
inc de
ld a,h
ld (de),a
ret

Lgint:
ld a,(hl)
inc hl
ld h,(hl)
ld l,a
ret

Lne:
R_crt1:
call Lcmp
ret nz
dec hl
ret

Lpchar:
ld a,l
ld (de),a
ret

Land:
ld a,l
and e
ld l,a
ld a,h
and d
ld h,a
ret

Lcmp:
ld a,e
sub l
ld e,a
ld a,d
sbc a,h
ld hl,1
jp m,Lcmp1
or e
ret
Lcmp1:
or e
scf
ret

Pourtant, si le compilateur C était efficace, je devrais obtenir le même code assembleur en 7 lignes que celui de la ROM du ZX-81.

L’exemple est un peu extrême, car ce compilateur (PhrozenC) est visiblement très inefficace. Ceci dit, même un bon compilateur (de cette époque ou de maintenant) produit un code 2 à 3 fois plus gros et plus lent que l’équivalent écrit à la main en code machine.
D’où la conclusion de tout programmeur qui veut écrire un logiciel raisonnablement rapide pour ce type d’ordinateur: il faut écrire entièrement en code machine.

L’incompatibilité des machines

Mais ici se pose un nouveau problème: un code machine dépend, par définition, d’un type de microprocesseur, et donc est incompatible avec d’autres microprocesseurs.
D’où le dilemme: soit on écrit en code machine et on obtient un logiciel petit et rapide, mais qui ne fonctionnera que sur des ordinateurs disposant du même microprocesseur, soit on écrit dans un langage plus évolué, comme le C, et on obtient un logiciel gros et lent mais plus portable.

Le système aussi est incompatible

À tous ces problèmes vient s’ajouter celui de l’incompatibilité des systèmes d’exploitation, la vraie plaie des années 80.
Même si deux machines ont le même microprocesseur, chacune a son propre système, en général écrit par le fabriquant de la machine.

Au début des années 80, lorsqu’on écrivait un logiciel destiné à plusieurs types de machines, il fallait écrire son architecture en conséquence. Le travail d’adaptation était tel qu’on appelait ça  » réécrire le logiciel pour telle machine« . Autant dire qu’écrire un logiciel pour 3 machines différentes demandait sans doute 4 ou 5 fois plus de travail que pour une seule. Certains logiciels tournaient sur une ou deux dizaines de plateformes, on imagine le travail nécessaire, et la maintenance cauchemardesque.

Il existait un semi-standard à peine émergeant: CP/M, mais il était sans doute trop « lourd » pour être largement installé sur les machines, et plutôt cher il me semble. Je ne l’ai personnellement jamais utilisé.

L’ensemble de ces difficultés explique pourquoi toute l’industrie de l’informatique s’est jeté sur les IBM PC: elle avait besoin d’un standard, pour n’écrire les logiciels et ne créer du matériel que pour une seule plateforme.

L’Amstrad CPC

Durant la première moitié des années 80, on voit une explosion des fabriquant d’ordinateurs, notamment entre 1982 et 1986.
À la fin de l’année 1984, un petit fabriquant d’électronique anglais, Amstrad, se lance sur le marché avec une machine simple et assez bien conçue. Son but n’est pas d’être révolutionnaire, mais d’être accessible aux familles, autant par son bas prix que par sa facilité d’installation et d’utilisation.

À cette époque, on avait le choix entre des machines hors de prix comme les Apple 2, et des ordinateurs plus familiaux comme les Commodore 64. Problème de ces derniers, et de leurs concurrents directs: la machine est vendue seule, il faut lui ajouter un magnétophone pour les sauvegardes et l’accès aux logiciels, un écran (en général une télévision), sans parler d’une éventuelle imprimante, le lecteur de disquette étant vraiment cher. Additionner tout cela pouvait monter facilement à environ 10000 FF, soit disons le tiers du prix d’une voiture.

D’où le choc à Noël 1984 lorsqu’ Amstrad annonce un ordinateur incluant le magnétophone et surtout l’écran, pour 4500 FF (en couleurs, sinon 3000 FF en N&B, il me semble). C’est la moitié du prix du matériel équivalent chez la concurrence.
De plus, bien que classique dans sa conception électronique, ses capacités sont bonnes, souvent meilleures que ses concurrents.

  • Processeur: classique 8 bits Z80 à 4 Mhz.
  • Mémoire 64 Ko. C’est vraiment beaucoup à cette époque, 2 à 4 fois plus que la concurrence.
  • Un vrai clavier, avec un pavé numérique. Le seul à son époque parmi les ordinateurs familiaux.
  • L’écran est de la qualité d’une télévision, mais sans tuner.
  • Le magnétophone est fiable (rare à l’époque), et il est même commandé par l’ordinateur (recherche automatique de fichiers, par exemple).
  • Un Basic intégré étendu très complet et rapide.

À l’époque, je pense que cette machine était une des plus astucieusement conçues, à mon sens globalement bien plus satisfaisante que la concurrence, et en plus bien moins chère.

Le choix classique du Z80 permettait à ceux qui avaient appris sa programmation en langage machine de pouvoir démarrer rapidement leurs essais sur cette toute nouvelle machine.

Au contraire du ZX-81, on peut dire qu’on avait enfin une machine qui permettait de vraiment programmer. Maintenant on pouvait enfin caser en mémoire à la fois un outil de programmation complet et le logiciel qu’on développait. Sur les machines moins puissantes, il fallait morceler les logiciels, ce qui est était lent et pénible à l’emploi.

J’aimerais vous montrer mes recherches de l’époque sur cette machine, mais malheureusement elles n’ont apparemment pas survécu au passage des cassettes aux disquettes.

La Bible du programmeur de l’Amstrad CPC : un sympathique pavé

En ce qui me concerne, c’est avec cette machine que j’ai vraiment commencé à explorer la programmation dans son ensemble. J’avais acheté un gros pavé, un livre appelé « La bible de l’Amstrad CPC » (maintenant téléchargeable en PDF), un classique pour comprendre et maîtriser la machine.

Ce type d’ouvrage est divisé en trois parties:

    * L’électronique de la machine.
    * Le système d’exploitation.
    * Le langage Basic.

Le système y était entièrement décompilé, c’est à dire que dans un premier temps un logiciel (un décompilateur) transforme le contenu de la ROM du système en un code source représentant les instructions machine du Z80 et les données, et dans un deuxième temps plusieurs personnes se sont donné le mal de comprendre et de commenter les 32 ko de la ROM. J’imagine combien de semaines il leur a fallu pour décrypter tout ce code, en expérimentant, en testant les routines système, etc.

Le plus passionnant, c’était de comprendre comment cette machine fonctionnait, d’en disséquer les composants tout autant que la programmation.
Et puis faire le lien entre les routines du système et l’explication sur le fonctionnement des circuits électroniques de la machine. Le but étant de pouvoir écrire un logiciel en langage machine qui puisse programmer directement les circuits, pour avoir un contrôle complet et une vitesse d’exécution maximale.

Autant dire que de telles notions apportent une excellente compréhension des concepts fondateurs des ordinateurs actuels:

  • programmation des circuits (parfait pour écrire des pilotes de périphérique aujourd’hui),
  • optimisation en langage machine (parfait pour programmer les micro-contrôleurs et les systèmes embarqués aujourd’hui, tels que les robots ou les ordinateurs d’avion ou de drone),
  • gestion des ressources, du partage du temps, des interruptions (parfait pour programmer des systèmes d’exploitation),
  • bibliothèque de routines de base (équivalent de la bibliothèque de base de C, ce qui fait qu’on comprend comment ce genre de chose s’écrit et fonctionne en pratique).

Durant mon travail actuel de programmeur, de temps en temps le souvenir d’une technique apprise sur cette ancestrale machine m’aide à résoudre un problème ou simplement à me faire une idée du fonctionnement d’un concept. J’ai par exemple appris dans ce livre comment générer des nombres pseudo-aléatoires, c’est une notion qui continue de m’être utile aujourd’hui. Pareil lorsque j’ai lu comment dessiner une ligne en mode graphique avec des méthodes optimisées, ce type d’optimisation m’a souvent servi dans des domaines pourtant très différents.

Voila une expérience que je suis content d’avoir vécu, car ce fut une époque de découverte, de défrichage, qui ne peut exister qu’une fois dans l’histoire d’une technique: à son départ.
Bien évidemment, on peut fixer le vrai départ de l’informatique dans les années 50, et surtout 60, mais il existe un vrai parallèle dans le monde de l’informatique « accessible », lorsqu’elle a vraiment atteint en masse l’industrie et les familles.

Ceci dit, la machine qui a été pour moi celle du commencement d’une programmation plus évoluée et riche, ça a été l’Atari ST.

L’Atari ST

Avec cette machine, une ère nouvelle s’ouvrait enfin au large public: l’interface graphique.

L’interface graphique est un concept essentiellement né dans les fameux laboratoires du Xerox PARC, durant les années 70.
Mais Xerox n’a pas vraiment produit de machine qui ait pu atteindre le public, que ce soit en entreprise ou en famille. Seuls quelques universitaires avaient accès aux prototypes de Xerox. D’où le mystère qui entourait ce nouveau concept d’interface utilisateur.

En 1985, il existait bien sûr une première matérialisation du concept grâce à Apple et son Lisa, suivi du Macintosh.
Malheureusement, le prix nettement prohibitif de ces machines les éloignait du grand public. Certes certaines entreprises les utilisaient, mais seules quelques personnes y avaient accès puisqu’il s’agit d’un ordinateur personnel.

J’étais jeune et j’ignorais presque tout de l’interface graphique, j’étais imprégné des interfaces textuelles classiques. J’avais certes lu des articles présentant le concept de l’interface graphique et aussi le Lisa, mais franchement il est nécessaire de manier une telle interface pour la comprendre, les mots et même les images sont insuffisants. Il faut la toucher, par la souris, la bouger, avec ses fenêtres, pour en ressentir le mouvement et la logique.

Ça a donc été un choc lorsque j’ai vu pour la 1ère fois un Atari ST, d’abord dans des articles puis dans des magasins.
Cette machine se présentait comme un concurrent du Macintosh, il avait clairement été conçu dans ce sens, mais il était à la fois plus puissant et beaucoup moins cher (à mon souvenir, environ 3 fois moins cher, c’est dire que les marges bénéficiaires d’Apple était déjà très confortables à l’époque).

Voyons à quoi cela ressemble:

  • Un microprocesseur 16/32 bits, le Motorola 68000, à 8 Mhz, excellent.
  • 512 Ko de RAM. Vraiment impressionnant, le Macintosh n’ayant que 128 Ko.
  • Une souris à deux boutons.
  • Un lecteur de disquettes 3,5 pouces, un futur standard (naissant à cette époque) de 720 ko.
  • Un clavier complet, avec des touches de fonction, un pavé numérique.

Bien qu’annoncé en 1985, L’Atari ST est arrivé en France véritablement en 1986, soit deux années après le Macintosh.

Autant l’Amstrad CPC était une machine bonne et bien conçue, mais très classique quant à son électronique, autant l’Atari ST nous faisait passer dans une nouvelle période. Tout était différent: un microprocesseur à code 32 bits, une RAM gigantesque permettant enfin de faire de grandes choses, l’interface graphique qui bousculait les habitudes, et à un moindre degré le lecteur de disquettes enfin devenu une norme (finies les cassettes audio lentes et moins que fiables), et puis aussi un clavier complet presque digne d’un ordinateur professionnel (sauf que le toucher était trop « rebondissant » pour moi).

Sa seule vraie concurrence était le Commodore Amiga, sorti lui aussi avec beaucoup de retard, mais bien plus cher que l’Atari et selon moi moins fourni en logiciels. J’en ai acquis un également, mais plus tard, à cause de son prix, ce qui fait que je ne l’ai jamais autant « pratiqué » que l’Atari; l’Amiga était une excellente machine, qui aurait mérité une meilleure finition de son système d’exploitation (mince, je vais relancer la gué-guerre ST contre Amiga).

Le Motorola 68000

Un programmeur plus jeune pourrait se demander en quoi un microprocesseur 68000 était vraiment différent d’un Z80. On pourrait en effet croire que la différence principale était la vitesse (environ 1 Mips contre 0,2) ou le passage des accès mémoire de 8 à 16 bits. Mais en fait il s’avère qu’une architecture différente peut représenter en soi une avancée et une ouverture vers d’autres possibilités.

Le Z80 est un bon processeur 8 bits, mais fondamentalement son jeu d’instruction est limité à 8 bits pour les données et 16 bits pour les adresses, il n’est pas évolutif. D’ailleurs dans la pratique ce processeur n’a pas eu de suite (commercialement viable en tout cas).
Au contraire, le jeu d’instructions du 68000 avait été minutieusement conçu pour l’avenir. Il était conçu dès le départ pour manipuler des adresses sur 32 bits (une taille restée valable jusqu’en 2011 environ), et des données également sur 32 bits pour l’essentiel. En d’autres termes, un logiciel conçu en 1979 pour un 68000 devait pouvoir tourner sur un descendant du 68000 en 2011, soit pérenne durant 33 ans. Pas mal, pour un milieu aussi mouvant que l’informatique.

À cette époque, le grand concurrent du 68000 était l’Intel 8086. Tout ceux qui ont programmé sur les deux savent à quel point utiliser le 8086 était une torture, en comparaison. Autant le code du 68000 était élégant, facile à lire et à écrire, autant celui du 8086 était tortueux, inutilement complexe et lent.

Pour ce qui est du reste de l’électronique du ST, elle était assez classique, sobre et bien conçue, mais pas révolutionnaire. C’était un ordinateur conçu pour le travail en entreprise, mais il n’a finalement pas rencontré ce marché. Toutefois il a trouvé son public dans les familles.
Son système d’exploitation marque le début d’une nouvelle époque: dorénavant cette partie de la machine demande beaucoup de travail à son fabriquant, sans doute trop pour les petits fabriquant qui avaient fleuri au début des années 80, et qui du coup ont tous brutalement disparu, voir Sainclair, Oric, Amstrad, Acorn, tous les MSX.

En 1986, le ST est donc une machine très intéressante, et ses capacités atteignent enfin un niveau qui permet de programmer à l’aise et de produire des logiciels puissants. Par contre, l’apprentissage de la programmation des interfaces graphiques représente un véritable cap à franchir pour tous les programmeurs qui ont passé beaucoup de temps sur des questions d’optimisation et d’utilisation directe des circuits.
Avec le ST, l’optimisation reste de mise car ces machines ne sont pas encore assez puissantes pour pouvoir se reposer sur un compilateur, mais d’un autre côté, la programmation d’un logiciel à interface graphique n’est pas du tout adaptée au langage machine et oblige donc dans ce cas à utiliser un compilateur (C ou Basic, principalement, sur cette machine).
On peut dire que le programmeur a donc le choix entre deux façons de travailler: l’une plus tournée vers l’efficacité et donc vers les jeux et le multimédia, et l’autre plus vers l’interface et donc vers les logiciels professionnels.

Le premier choix est le plus technique, celui qui réclame le plus de finesse, d’imagination, de maîtrise de la machine, il est aussi sans doute le plus attirant lorsqu’on est jeune, créatif, et qu’on se lance des défis.
Je n’y ai pas échappé, et j’ai donc expérimenté ce type de programmation. Voyons un peu comment ça se passe en pratique.

Claustrophobia

Une vidéo est disponible sur Dailymotion. Malheureusement, on dirait que le lecteur flash la limite à 25 fps au lieu de 50. Moi qui était si fier de mes 50 fps ! 😉







C’est un jeu en 2D dans lequel une tête se balade dans un château en volant, et doit récupérer ses membres perdus. On se demande comment cette tête peut se déplacer sans jambe et même sans corps, mais peu importe, elle vole !

D’un point de vue technique, déplacer cette tête a été un vrai casse-tête, justement.
Je ne voulais pas faire des « salles » séparées par des portes comme au temps des ordinateurs 8 bits, donc c’est le décor qui devait défiler.
Problème: l’électronique du ST ne permettait pas de faire défiler un écran, il fallait réécrire tout l’écran à chaque déplacement, du moins avec les premières versions du ST.
Comme le microprocesseur était un peu trop lent pour parvenir à redessiner l’écran 50 fois par seconde (oui, j’aime les défilements parfaits), j’ai dû à la fois réduire la taille de la zone de jeu (d’où les bordures vertes) et réduire le nombre de couleurs du décor. En fait le décor est réduit à quatre petites couleurs, alors que l’ordinateur pouvait en afficher 16. En nombre de bits, j’avais donc réduit la quantité de données de moitié par les couleurs, plus une petite réduction par les bordures, donc ouf!, l’animation était finalement parfaite.

Pour compenser le manque de couleurs du décor, j’ai intercepté les interruptions de « balayage horizontal » pour faire varier les couleurs verticalement.
Le balayage, c’est l’interruption qui se produit à chaque début de traçage d’une ligne horizontale de l’écran, soit environ 15000 fois par seconde. En termes de langages de programmation modernes, on appellerait peut-être ça un « évènement », mais ici il s’agit d’interruptions matérielles générées par le circuit graphique et directement reçues par le microprocesseur.

Étant donnée que le processeur ne fonctionne qu’à environ 1 Mips, 15000 interruptions à traiter chaque seconde consomment une bonne partie de son activité, d’où la nécessité de réutiliser ces interruptions pour générer en même temps les sons numériques.
Je rappelle qu’à cette époque, c’était le tout début des sons numériques sur ordinateurs. Auparavant, seuls des sons synthétiques étaient joués, d’où des bruitages simplistes mais mémorables dans les jeux d’arcade.

L’ennui avec le ST, c’est qu’il n’a pas été conçu comme une console de jeux (contrairement à son concurrent l’Amiga, oui j’attise la gué-guerre), et ne disposait pas d’un DMA pour gérer le son. Il devait ‘à la main’ modifier le son, quelques dizaines de milliers de fois par seconde.

Cet exemple de double utilisation d’une interruption, montre que l’optimisation d’un logiciel devait être globale, ici il a fallu programmer entièrement en langage machine, car presque chaque cycle d’instruction comptait.

Plus haut, une image montre l »écran d’accueil de la démo, qui présentait des couleurs.
Cet écran illustre bien la possibilité du changement de palettes. Chaque ligne horizontal affiche ici 24 couleurs différentes. Sachant que le ST était normalement limité à 16 couleurs pour la totalité de l’écran, on voit bien qu’il a fallu modifier cette palette à chaque ligne, et même plusieurs fois par ligne.

Voyons un peu à quoi ressemble le code machine 68000.

En résumé, le 68000 dispose de 8 registres de données en 32 bits et 8 registres d’adresse en 32 bits (servant de pointeur). Il n’existe pas de registre particulier (type Accumulateur en Z80 ou certains autres en 8086, que j’ai oublié – c’est dire si ce dernier processeur m’a peu passionné). Tous les registres de données sont généraux.

Par chance, il me reste une partie de mes codes sources de l’époque. J’ai quand même dû écrire un logiciel pour pouvoir relire les disquettes sous Windows, à cause de limitations de Windows NT.

e2i38:
move.l #e2coul1,$ffff825a.w
move.b #1,$fffffa21.w
move.l #e2i39,$120.w
bclr #0,$fffffa0f.w
rte

e2i39:; Avant-dernière inteeruption HBL.
move.l #e2coul1,$ffff825a.w
move.b #1,$fffffa21.w; laisser toujours 1.
move.l #e2i40,$120.w
bclr #0,$fffffa0f.w
rte

e2i40:; Dernière interruption HBL.
move.l #e2coul2,$ffff825a.w
;bset #3,$fffffa17.w
bclr #0,$fffffa0f.w
bclr #0,$fffffa13.w
rte

Cet exemple montre des routines qui traitent l’interruption des lignes d’écran (« HBL = horizontal blank line »).
On peut remarquer qu’elles se chaînent (par le move.l #e2i39,$120.w). En effet, au lieu de lire le contenu d’un tableau par indirection et d’incrémenter un index à chaque appel, j’ai préféré que chaque routine ne lise qu’une simple variable, puis elle modifie le vecteur (l’adresse de la routine) de l’interruption.
Ainsi, le code est plus rapide. Par contre il a nécessité l’écriture de 40 routines différentes. J’avais bien sûr utilisé un générateur de macros pour les créer.
Le gain peut paraître faible, mais il a lieu plus de 15000 fois par seconde. Sur ce type de machine, il faut savoir économiser le moindre cycle d’instruction.

À noter que des instructions d’indirection complexe sont apparus avec les versions suivantes du 68000. De plus le cache RAM change le calcul du temps d’exécution sur ces processeurs.
L’optimisation n’est pas toujours une activité pérenne, elle n’est efficace que temporairement, dans un contexte précis. Toutefois, ses principes restent valables et utiles.

La gué-guerre 68000 contre 8086

Cet exemple montre également que le 68000 gère effectivement les données et les adresses en 32 bits.
À titre de comparaison, son concurrent, le 8086, ne gère que des données en 16 bits et doit donc s’y prendre à plusieurs fois pour une seule donnée. De plus, sauf erreur, le 8086 ne peut pas écrire une valeur dans une variable (une case mémoire) en une seule instruction: il doit passer par l’intermédiaire d’un registre. D’où le véritable jonglage permanent de registres, d’autant que ses registres sont, en plus, spécialisés: on ne peut tout faire avec n’importe quel registre. Et hop, vive la jongle !
Et je ne parle pas de cette ignominie du 8086 que sont les Segments.
On voit ici par contre que le 68000 peut en une seule instruction écrire une valeur 32 bits dans une variable.

Si je voulais être mauvaise langue, je dirais qu’Intel et Microsoft ont ceci de commun qu’ils n’ont jamais eu besoin de sortir de bons produits, car il leur suffisait de représenter un standard.
Bon, c’est un peu faux car Intel a quand même eu un peu de concurrence avec AMD, durant quelques courtes années. 😉

Enfin bon, les compilateurs cachent toutes les plaies (c’est à dire le code machine).

Les outils improbables

Pour mon travail, j’avais aussi créé des tas d’outils en tous genres, qui peuvent paraître bizarroïdes maintenant, mais qui à l’époque me rendaient de bons services.

  • L’assembleur en Reset
    Mon outil préféré était un assembleur-désassembleur-débogueur-éditeur appelé K-Seka. Il était très pratique car il avait tout en un seul programme. Un peu un IDE rudimentaire, quoi.
    Mon problème était qu’à cette époque il n’existait pas de protection de mémoire dans le système d’exploitation, donc un logiciel pouvait joyeusement faire planter le système entier !
    Inutile de dire qu’écrire un jeu, lequel manipule les circuits intégrés directement, ça fait planter souvent.
    J’avais donc modifié cet assembleur pour intercepter la remise à zéro du ST, lorsqu’on presse la touche Reset à l’arrière de la machine, et remettre le système en état de fonctionner, notamment remettre tous les circuits dans un état valide, puis donner la main au débogueur de K-Seka.
    Imaginez: vous lancez votre jeu, il plante, vous pressez le bouton Reset, et hop!, vous vous retrouvez dans votre IDE. Magique !
    Quel gain temps, comparé avec devoir redémarrer la machine et recharger l’IDE.
  • Un filtreur graphique en simili-4096 couleurs
    Je m’en servais pour récupérer les images du numériseur, via un magnétoscope, et pour les traiter: réduction à 512 ou à 16 couleurs avec des contraintes de palette, détramage, débruitage, moyennage sur plusieurs images (vue la faible qualité des magnétoscopes), ajustage de la luminosité, séparation du fond, etc.
    J’avais besoin d’afficher les images dans un maximum de couleurs, or comme le ST était limité à 16 couleurs simultanées, j’utilisais à la fois la technique du changement de palette 3 fois par ligne, plus un clignotement (tramé pour ne pas trop me bousiller le cerveau), et enfin un tramage en affichage.
  • Des éditeurs de niveaux
    Forcément, ils sont nécessaires pour créer le décor des jeux, placer les obstacles ou les personnages, etc.
    Je pouvais lancer une version limitée du jeu à tout moment pour tester le niveau, puis revenir à l’éditeur.
    Ce type d’outil était programmé en Basic (avec GFA-Basic), car on programme beaucoup plus vite ainsi qu’en langage machine, avec moins d’erreurs, et que la vitesse d’exécution ne pose pas de problème.
    Le GFA-Basic permettait aussi d’écrire une interface graphique simple sans trop d’effort.
  • Un constructeur de macro-instructions
    Pour des besoins particuliers, j’avais besoin de créer des morceaux de code machine assez longs et répétitifs mais nécessaires pour ne pas perdre de précieux cycles d’exécution.
    Le 68000 disposait par exemple d’une instruction Movem qui pouvait à elle seule empiler plusieurs ou même tous les registres du processeur. Il était courant de la détourner de son but pour copier ou écrire des blocs complets de mémoire avec un minimum d’instructions.
    « movem.l d1-d7/a0-a5,-(a6) » peut ainsi écrire 52 octets en une seule instruction. Toutefois, cela occupe les registres. J’avais donc écrit différentes macros pour gérer les instructions et les registres en fonction de l’effet désiré, avec des paramètres.
    C’est typiquement le genre d’outil qui aujourd’hui ne s’utiliserait que dans un contexte très particulier, comme dans une routine essentielle dans un jeu, ou dans une analyse ou un tracé en boucle pour un robot par exemple.
  • Un formateur de disquettes avec sur-capacité et « protection »
    Une fois que j’ai su programmer le circuit contrôlant le lecteur de disquettes, j’ai commencé à jouer avec la capacité des disquettes. Sur le ST on pouvait ainsi facilement passer de 720 ko à 800 ko en toute sécurité, voire 880 ko avec des ralentissements et une fiabilité incertaine.
    On pouvait aussi formater d’une façon particulière pour éviter les copies pirates. À cette époque il y avait une vraie escalade entre les « déplombeurs » et les éditeurs de logiciels. Chaque nouveau type de protection entrainait une réplique sous forme d’un logiciel de copie.
    Cela rendait ce domaine assez stratégique et également intéressant d’un point de vue intellectuel.
    Ce genre de connaissance m’a permis d’écrire récemment un logiciel de récupération des vieilles disquettes ST, sous Windows. Il m’a fallu réviser le mode MFM , pour me rappeler des codes de correction de données et autres joyeusetés, ça m’a un peu rappelé le bon vieux temps.🙂
    À l’époque j’avais écrit un mode de protection de disquette dont je m’étais servi pour envoyer une démo de jeu à des éditeurs. Je me rappelle encore l’un d’eux se plaignant de n’être pas parvenu à copier la disquette pour ses collaborateurs. Ah ah ! Il avait essayé tous les logiciels de copie du marché, en vain, ça m’avait bien fait rire (ce qu’on peut être bête, à c’t’âge ! Je parle de moi..).
  • Un module lecteur de fichiers en multi-tâches
    Il permettait de charger des fichiers en RAM durant un jeu tout en continuant l’activité en cours. Par exemple, on pouvait jouer à un niveau pendant que le ST chargeait le prochain niveau.
    Ce genre de chose nécessitait une bonne connaissance du contrôleur de disquette, pour tout gérer par interruptions. En comparaison, le système du ST bloquait tout l’ordinateur lorsqu’il accédait à un fichier, et il ne savait pas accèder en parallèle à plusieurs fichiers.

Etc. J’ai presque tout oublié, tellement ces outils étaient nombreux et souvent particuliers à un besoin temporaire.
C’était vraiment l’époque de la débrouille et de la créativité. Il fallait à tout moment être assez inventif et ingénieux pour trouver une méthode qui permettrait d’obtenir un meilleur résultat, ou bien qui faciliterait ou raccourcirait le travail.
D’où ma façon personnelle de programmer: faire plus au départ pour faire beaucoup moins sur l’ensemble d’un projet. Ce que certains appellent perdre du temps pour mieux en gagner.

Car crash

Une vidéo est disponible sur Dailymotion. Malheureusement, on dirait que le lecteur flash la limite à 25 fps au lieu de 50.

C’est un jeu de voitures, comme on peut le voir.





Tramage et clignotement:
La première image introduit le jeu, le temps de le charger (en environ 40 secondes).
Pour améliorer l’image, j’ai utilisé un système de clignotement des pixels, histoire de simuler un dégradé de 16 niveaux d’orange au lieu de 8.
Mais là où j’ai passé du temps, c’est sur une chose qui ne se remarque pas: la structure du « tramage ». J’appelle tramage l’effet de points qui comble en quelque sorte le manque de niveaux dans le dégradé. Il existe des formules de tramage toutes faites, mais elles donnent un résultat soit trop grossier soit trop régulier. D’où du temps passé à chercher une meilleure méthode, adaptée à une basse résolution et de gros pixels. C’est par exemple un domaine de recherche pour les ingénieurs travaillant pour les fabriquant d’imprimantes.

Palette variable verticalement:
Sur les images du jeu proprement dit, on voit que pour améliorer les couleurs (problème récurrent sur ces machines, jusqu’en 1993 environ), j’ai utilisé tout autant le tramage (notamment dans le ciel) que le changement de palette vertical par interruption (comme décrit ci-dessus pour Claustrauphobia).

Calculs en 3D:
Contrairement à presque tous les jeux de voiture de l’époque, les calculs de la route étaient en vraies 3D. Il faut savoir que ce type de microprocesseur ne dispose pas d’unité de calculs en virgule flottante, et qu’une simple multiplication entière 16 bits * 32 bits prend environ 60 à 200 cycles d’instruction (à mon souvenir). Lorsqu’on doit effectuer beaucoup de calculs, et qu’on tente d’obtenir 50 images par seconde, ça devient vite un casse-tête qui nécessite des tas de caches de pré-calculs et d’optimisations diverses et variées.

Zoom pré-calculé et étirement vertical:
Les « sprites » (les images qui se déplacent sur l’écran) avaient, pour leur zoom, une taille variable pré-calculée (encore une petite optimisation), mais comme cela occupait finalement beaucoup de place en RAM (imaginez: toutes les tailles affichables d’un sprite !), j’avais fait une autre optimisation spéciale aux tailles les plus grandes: les lignes horizontales y étaient dupliquées lors du dessin.
Sur une image, on voit que le palmier le plus grand est en quelques sortes « étiré » en hauteur, ses pixels sont rectangulaires, plus hauts que larges. C’est donc essentiellement pour une économie de RAM.
Petite astuce, grosse économie de RAM.

Le jeu était sensé comporter un aspect aventures, j’avais donc présenté des images fixes de décors accompagnant l’histoire.

La difficulté était que le ST ne dispose que de 16 couleurs simultanées, choisies parmi 512. On est loin des 16 millions modernes.
Pour pallier ce manque de couleurs, j’ai utilisé une technique assez classique sur cet ordinateur: afficher 16 couleurs différentes sur chaque ligne horizontale, ce qui permet de démultiplier les possibilités.
En fait c’est même plus que ça car on change trois fois la palette de 16 couleurs sur chaque ligne horizontale.
Si on regarde attentivement cette image, on voit qu’il y a parfois des petits traits pleins horizontaux: c’est lorsque l’algorithme que j’avais créé pour la circonstance n’a pas pu trouver assez de couleurs à un endroit donné, malgré toutes les variations de palette.
C’est beaucoup de complications pour un peu plus de couleurs. Le principal problème, c’est que cela occupe complètement le microprocesseur, et ce durant environ 64 % du temps. À noter que l’Amiga n’a pas ce problème puisqu’il dispose d’un circuit programmable capable de gérer à lui seul ce type de changement de palette, sans compter son mode HAM.

Conclusion

Comme on peut le voir, ce type de programmation consiste à passer des jours à optimiser chaque petit détail à peine visible à l’écran, mais dont la somme fera au final la différence entre un logiciel rapide et un autre saccadé (et souvent moche en prime).

Voila pourquoi selon moi c’est un genre de programmation très pointu, très formateur car on y apprend toutes sortes de techniques. Il nous rend très minutieux à cause des nécessaires optimisations, et très prévoyant car une erreur de structure dans un programme en langage machine oblige souvent à le restructurer en grande partie.

J’ai aussi écrit ce petit article car bien souvent lorsqu’un programmeur dit qu’il écrit des jeux, les professionnels du secteur informatique l’imaginent en train de jouer devant sa console toute la journée, alors que c’est bien souvent l’inverse: ce type de travail demande plus de technicité, de sérieux et de compétence que bien d’autres travaux de programmation.
En ce qui me concerne, créer une application bureautique me demande moins d’efforts, ce sont presque des vacances, en comparaison, du boulot bien pépère ! 😉

De l’époque des premiers ordinateurs familiaux, j’ai gardé le goût du travail bien exécuté, bien pensé au départ, minutieux et pérenne, et aussi la passion d’une certaine forme de programmation, où la créativité a autant sa place que le sérieux, car les deux se complètent.

Espérons que dans le futur la programmation reste un plaisir pour tous les programmeurs ! 🙂

 
Poster un commentaire

Publié par le 2012-09-03 dans Programmation

 

Différence en C# entre new et override lors du remplacement d’une fonction à hériter

Au départ, un débutant en C# se demande quelle peut bien être la différence entre new et override, et lequel choisir lorsqu’il souhaite remplacer une méthode dans une classe héritante.

Vocabulaire utilisé ici

  • Classe à hériter = classe de base.
  • Classe héritante = classe dérivée (elle hérite de la classe de base).

En une phrase

Avec override (et donc virtual), le code de la classe à hériter (la classe de base) appellera soit sa propre méthode virtual, soit la méthode d’une classe héritante (dérivée de cette classe de base) si celle-ci la remplace avec override.

Exemple montrant la différence

Le code:

public class ClasseÀHériter
{
    public string FonctionNormale()
    {
        return "FonctionNormale dans la classe de base";
    }
    public virtual string FonctionExplicitementRemplaçable()
    {
        return "FonctionExplicitementRemplaçable dans la classe de base ";
    }
    public string Résultat()
    {
        return
            this.FonctionNormale()  // Appelle toujours la fonction définie dans cette classe.
            + "\n"
            + this.FonctionExplicitementRemplaçable(); // La fonction appelée a pu être remplacée dans une classe héritante.
    }
}

public class ClasseHéritante : ClasseÀHériter
{
    public new string FonctionNormale()
    {
        return "FonctionNormale dans la classe dérivée";
    }
    public override string FonctionExplicitementRemplaçable()
    {
        return "FonctionExplicitementRemplaçable dans la classe dérivée";
    }
    public string AutreRésultat()
    {
        return
            this.FonctionNormale()
            + "\n"
            + this.FonctionExplicitementRemplaçable();
    }
}

public MainWindow()
{
    InitializeComponent();

    // ------------------

    ClasseÀHériter càh = new ClasseÀHériter();
    string scàh = càh.Résultat();

    ClasseHéritante ch = new ClasseHéritante();

    string schr = ch.Résultat();
    string scàhar = ch.AutreRésultat();
}

Résultats:

Classe utilisée Méthode appelée Texte renvoyé par FonctionNormale() Texte renvoyé par FonctionExplicitementRemplaçable()
ClasseÀHériter Résultat() de ClasseÀHériter FonctionNormale dans la classe de base FonctionExplicitementRemplaçable dans la classe de base
ClasseHéritante Résultat() de ClasseÀHériter FonctionNormale dans la classe de base FonctionExplicitementRemplaçable dans la classe dérivée
ClasseHéritante AutreRésultat() de ClasseHéritante FonctionNormale dans la classe dérivée FonctionExplicitementRemplaçable dans la classe dérivée

Interprétation:

  • Lorsqu’on instancie ClasseÀHériter, elle utilise ses propres fonctions.
  • Lorsqu’on hérite de ClasseÀHériter dans ClasseHéritante, la méthode FonctionNormale() appartenant à ClasseÀHériter utilise la méthode FonctionExplicitementRemplaçable() remplacée par ClasseHéritante avec override.
  • Lorsqu’on hérite de ClasseÀHériter (dans ClasseHéritante), la méthode AutreRésultat() de ClasseHéritante utilise les deux méthodes remplacées (avec new et avec override).

Aspect technique

Alors que new se contente de définir une méthode dont le nom remplace celui de la classe à hériter, override redirige les appels à la méthode remplacée même pour le code de la classe de base à hériter.
Cela implique de placer dans la partie statique de la classe à hériter, et dans toutes ses dérivées, un pointeur (un délégué, si vous voulez) qui permet au code de la classe à hériter de trouver un éventuel remplacement (par override) dans les classes héritantes.

Simulation de la virtualisation

Pour comprendre le mécanisme, voici une sorte d’écriture manuelle d’une méthode virtualisée dans une classe:

public class SimulationDeVirtualisation
{
    public delegate string ModèleDeLaMéthodeÀVirtualiser();

    public ModèleDeLaMéthodeÀVirtualiser RedirecteurVersLaMéthodeÀUtiliser; // En fait devrait être statique.

    public SimulationDeVirtualisation() // Constructeur:
    {
        RedirecteurVersLaMéthodeÀUtiliser = MéthodeDebaseRemplaçable;
    }

    private string MéthodeDebaseRemplaçable()
    {
        return "Méthode dans la classe de base";
    }

    public string DitSiMéthodeRemplacée()
    {
        return RedirecteurVersLaMéthodeÀUtiliser();
    }
}

public class ClasseHéritanteAvecMéthodeVirtualisée : SimulationDeVirtualisation
{
    public ClasseHéritanteAvecMéthodeVirtualisée() // Constructeur:
    {
        RedirecteurVersLaMéthodeÀUtiliser = MéthodeDebaseRemplaçante;
    }

    static private string MéthodeDebaseRemplaçante()
    {
        return "Méthode dans la classe dérivée";
    }
}
   
public MainWindow()
{
    InitializeComponent();

    // ------------------

    SimulationDeVirtualisation sv = new SimulationDeVirtualisation();
    string tsv = sv.DitSiMéthodeRemplacée();    // Renvoie "Méthode dans la classe de base".

    ClasseHéritanteAvecMéthodeVirtualisée chamv = new ClasseHéritanteAvecMéthodeVirtualisée();
    string tchamv = chamv.DitSiMéthodeRemplacée(); // Renvoie "Méthode dans la classe dérivée".
}

Je n’ai pas pu implémenter aussi parfaitement que je l’aurai voulu, à cause de limitations de C#, mais on voit bien que la classe de base utilise soit sa propre fonction virtuelle, soit la fonction remplaçante implémentée dans la classe dérivée.

À noter que RedirecteurVersLaMéthodeÀUtiliser devrait être statique, aussi bien dans la classe de base que dans chaque dérivation de cette classe, et que dans ce cas il est inutile de l’initialiser dans le constructeur de chaque classe.

Bien entendu, le compilateur C# s’occupe de ces détails à notre place lorsqu’on utilise les mots-clés virtual et override.
Mais il est bon de savoir ce qu’implique cette utilisation, en terme de ressources et de performances.

Conclusion(s)

  • Virtual vous est utile lorsque vous écrivez une classe à hériter (une classe de base), et que vous voulez y utiliser des informations qui n’existeront que dans les classes qui en hériteront.
  • L’utilisation de Virtual entraîne l’ajout d’un pointeur caché (au sens du C) dans la partie statique des classes à hériter et des classes héritantes, et une redirection dans le code de la classe à hériter. Soit un peu plus de complication.
    Mieux vaut donc éviter de créer des classes virtuelles et leur préférer des remplacements par new lorsque c’est possible. Surtout pour des petites classes souvent instanciées.
  •  
Poster un commentaire

Publié par le 2012-06-24 dans Programmation

 

Étiquettes : , , , , , , , , , ,

 
Suivre

Recevez les nouvelles publications par mail.