May 21, 2023

Interoperability - תאימות

תכונות התוכנה

כדי לכתוב קוד לא צריך לחשוב עמוק מדי על יכולות התוכנה שאנחנו הולכים לכתוב.
לעיתים תוכנה נוצרת כתוצאה מצורך יחידני ולאחר מכן הוספה של פתרונות לצרכים אחרים.
למשל תוכנת ה-IDE - Integrated Development Environment.

בפעם הראשונה היה צורך בתוכנה שמציגה את הקוד וחלון פקודות כדי לקמפל ולהריץ את הקוד.
לאחר מכן היה צורך לדבג את הקוד ולראות את המידע אודות הריצה - זכרון, שגיאות וכדו’
ולבסוף נוצרו תוכנות עם יכולות מתקדמות כמו ריצה של תהליכים מרובים, דיסיינגרים של חלונות ועוד…

לכל אורך תהליך הפיתוח, הם קיבלו פידבקים וחשבו על אילו עוד פיצ’רים המתכנתים רוצים.
כל יכולת של התוכנה מוסיפה לפונקציונאליות של התוכנה.
וזו הייתה ההתמקדמות הראשונית שלהם.
הפונקציונליות היא התכונה הראשונה לכל תוכנה - מה היא מסוגלת לעשות.

ככל שהתוכנה גדלה התחיל הצורך גם לחשוב מעבר לפונקציונליות,
האם צריך תמיכה בשפות נוספות?
האם הפיצ’רים החדשים גורעים מהביצועים?
האם שפת הפיתוח השתנתה וצריך לתמוך בשפות פופולריות נוספות?

כל השאלות האלו נענות בעזרת הגדרת תכונות העל של התוכנה.

יש אף סטנדרט שמגדיר את התכונות והתתי תכונות הנקרא - ISO/IEC 25010.

חלק מהתכונות הגדולות הן:

  • פונקציונליות
  • אפקטיביות וביצועים
  • שימושיות
  • אבטחה
  • תחזוקה
  • תאימות ושימושיות בין מערכתית

אנחנו נתמקד באחרון מביניהם - תאימות ושימושיות בין מערכתית.
או במילה אחרת - Interoperability.

Interoperability - תאימות

התכונה הזו מתארת כיצד התוכנה שלנו יכולה לעבוד מול מערכות אחרות.
לעיתים נפגוש את התכונה הזו בשלב ה-אינטגרציות.

שלב האינטגרציות הוא השלב שבו לוקחים שתי מערכות ונותנים להם להתנשק.

וברצינות יותר, זה השלב הקריטי בו פוגשים את תת המערכות בתוך כל התהליך או המוצר.
כאן נשאלת השאלה - איך אנחנו מתכננים את זה מראש?
לא הכל כמובן נוכל לתכנן לפרטי פרטים, נציג כאן את הדרך הראשונה והמודרנית בהתמודדות עם כלים לתאימות בין מערכתית.

סנטדרטים

אחד העקרונות שעוזרים לתחום התוכנה להשיג תאימות בין מערכות אלו סטנדרטים.
סטנדרט בקצרה אומר - כולם עכשיו מבינים את אותה השפה.

בדרך כלל סטנדרטים מגדירים פרוטוקולים - ופרוטוקול מגדיר בתחום התוכנה איך המידע נראה וכיצד הוא מועבר.

סטנדרטים ופרוטוקולים נפוצים:

  • TCP
  • HTTP
  • FTP
  • OpenGL
  • SQL
  • XML

RFC - Requests For Comments

לרשימה:
https://en.wikipedia.org/wiki/List_of_RFCs

ואם אתם רוצים לצחוק קצת -
https://en.wikipedia.org/wiki/April_Fools%27_Day_Request_for_Comments

וכמו כן רשומות של הצעות להתנהגות תוכנתית:
https://en.wikipedia.org/wiki/Best_current_practice

Restful API and Json

Json

Json - JavaScript Object Notation.
לראשונה נוצר כחלק מ-js אך היום כלל השפות כוללות מודולים לניהול json.

הפורמט כתוב בצורה טקסטואלית שניתנת לדחיסה וגם לקריאה -

1
2
3
4
{
"name": "Bob",
"age": 42
}

וכך זה נראה דחוס:
{"name":"Bob","age":42}

REST API

Representational State Transfer API זו ארכיטקטורה תוכנתית לשירותים אינטרנטיים.
העקרונות הבסיסיים:

  • מודל שרת-קליינט
  • זיהוי משאבים על ידי מזהה ייחודי
  • ייצוג אחיד על מנת לעדכן את המידע בשרת
  • Stateless - כל בקשה לשרת תהיה אחידה ולא ישמור מידע בין בקשות.
    בעיקר כדי שלא יצטרכו לשלוח בקשה מסוימת לפני בקשה אחרת.
  • ניתן לשמור את המידע ב-Cache וההתנהגות לא תיפגע.
  • היררכיה ושכבות למידע.

בניגוד להסכמות מסוימות כדי להגדיר API כ-Restful הוא אינו חייב להיות ב-Http.
שירותים כמו C# WCF היו יכולים לעבוד עם שירותי REST גם ללא HTTP.

אולם HTTP ו-JSON אלו כלים בשימוש מאוד נפוץ וקל לפתח שירותי אינטרנט בעזרתם.

HTTP על רגל אחת

לשירות יש כתובת, למשל האתר של נאסה:
https://apod.nasa.gov/.

לכל משאב יש תת-כתובת, זו הצורה שבה אנחנו יכולים לשלב היררכיה ושכבות למידע שלנו.

למשל כדי לגשת לתמונה היומית של נאס”א נוכל לגשת ל-
https://apod.nasa.gov/apod/.

וכדי לגשת למשאב נוכל לציין במפורש איזה משאב אנחנו רוצים בכתובת:
https://apod.nasa.gov/apod/ap230520.html

לכל בקשה יש כמה דברים:

  • כתובת
  • מתודה/פעולה - GET/POST/PUT/DELETE
  • מידע על הבקשה - headers
  • במידה ויש לנו מידע לשלוח שלא כחלק מהכתובת אז - payload - גוף ההודעה.

בפוסט אחר הסברתי על כל אחד מהם:
הסבר על URL

הסבר על HTTP


שרת וקליינט בין שפות שונות

לאחר שהבנו את הבסיס להבנת השירותים - נוכל להיעזר בכלים שהצגנו על מנת לבנות מערכות מותאמות בשפות שונות.

את קוד הלמידה תוכלו למצוא כאן:

https://github.com/Ilya122/interoperability_learning_in_simplycode/tree/main/1_JsonHttp/MainServer

תורידו אותו ותוכלו להתנסות עם השרת ב-.net C#.
בשביל לדבג צריך Visual Studio.
ממליץ על ה-Community Edition 2022.

יצירת השרת ב-C#

ל-.Net ו-C# יש מנגנון פשוט שנקרא Minmal web API המבוסס Asp.Net.
כדי ליצור פרויקט חדש שלו נוכל לפתוח בעזרת הפקודה:

1
dotnet new web -o MainServer

לקרוא עוד על הטכנולוגיה

למשל ניקח את הבקשה הכי פשוטה:

1
app.MapGet("/", () => "Hello to Cat world!");

כאשר ניגשים ל-API בלי שום משאב זה יחזיר Hello to Cat World!.

שיפור השירות

כעת נוסיף התנהגות לשירות שלנו.
נוכל להשיג את כמות החתולים בחתוליה,
נוכל להשיג את התמונה של חתול ספציפי,
ונוכל לעלות תמונה של חתול.

בקשות בעזרת curl

אחרי שמריצים את הAPI

curl זוהי תוכנה נפוצה לשליחת בקשות.

לבדוק שהשירות למעלה:

1
curl -X GET https://localhost:7264/

לבדוק את כמות החתולים:

1
curl -X GET https://localhost:7264/cats

לבקש תמונה ספציפית של חתול:

1
curl -X GET https://localhost:7264/cats/1

לעלות תמונה של חתול:

1
curl -v -k -H "image/jpeg" -F file=@newCat.jpg https://localhost:7264/cats

שימו לב שהקובץ newCart.jpg קיים ב-Current Directory שאתם נמצאים בו.

כדי לבדוק שזה עובד תנסו לגשת לחתול שלישי - מכיוון שיש 2 חתולים זה יחזיר לא נמצא:
https://localhost:7264/cats/3

לאחר מכן הריצו את הפקודה למעלה כדי לעלות חתול:

עכשיו כשהבנו איך זה עובד אפשר ליצור את הקליינט שלנו.

אתר החתולים

כדי לבנות את האתר נשתמש ב-cherrypy ו-pyvibe.

CherryPy

https://docs.cherrypy.dev/en/latest/

פריימוורק מינימליסטי לאתרים

PyVibe

https://www.pyvibe.com/

חבילת פייתון לג’נרוט html.

לחץ כאן עבור הקוד של הקליינט

אינטגרציה וארכיטקטורה

השתמשנו בכלים פשוטים ביותר כדי לבצע אינטגרציה בין הצד שמכיל את המידע ה-Cat API שלנו.
שם גם ה-storage.

בלי “קישור ישיר” אנחנו בונים אתר ומחברים אותו ל-API בעזרת שני הכלים Http ו-Json.
כך אנחנו יכולים לברר אודות מצב החתולים בשטחים ולהשיג כל חתול בנפרד.

מה נפלא כאן?
את האתר בנינו בעזרת הכלים python, html ו-js.
איפה כאן מסתתר קוד javascript? בכפתור!
כאשר אנחנו מעבירים חתול אנחנו בעצם מבצעים הפנייה לג’אווה-סקריפםט.

את ה-api בנינו בעזרת C#.
למזלנו אין כאן צורך במסד נתונים אז חסכנו מאיתנו את הטרחה הזו!

אלטרנטיבות

נעשו במרוצת השנים טכנולוגיות שעובדות על גבי פרוטוקולים שונים כדי לנסות לפשט את צורת העבודה.

WCF

https://learn.microsoft.com/en-us/dotnet/framework/wcf/whats-wcf

טכנולוגיה ישנה אך המהות שלה לא השתנה - לקחת אובייקטים הכתובים ב-C#, לסרלז אותם ולשלוח אותם מעל פרוטוקול מסוים.

ProtoBuff

https://protobuf.dev/

השפה של גוגל לתיאור גנרי של אובייקטים על מנת לסרלז אותם ולשלוח אותם.


קישוריות בין שפות תכנות וה-ABI

מה זה ABI

בארכיטקטורה הקודמת יצרנו ממשק http בו אנו יכולים לגשת משפה אחת לשפה שנייה.
ממשקים בין מחלקות וקריאות http בדרך כלל נקראות - קריאות API.

API - Application Programming Interface.

בכללי API זה קוד שפונה לקוד אחר - ובדרך כלל איננו יודע על איך הוא ממומש.
אך יש שכבה מתחתיה שנקראת ABI הגורמת לזה לעבוד.

ABI - Application Binary Interface.

החיבור בין C++ ל-פייתון

הקוד נמצא בתיקייה הזו - יש להוריד אותה ולחלץ לתיקייה.

https://github.com/Ilya122/interoperability_learning_in_simplycode/tree/main/2_CPP_ABI/CPP_API

קודם כל נגדיר פרוייקט קטן שמחשב משהו בעזרת C++

1
2
3
4
5
6
7
#include "programExport.hpp"

class PROGRAM_EXPORT Calculator
{
public:
int Calculate(int a, int b); // Implementation in cpp file
};

ה-PROGRAM_EXPORT מוגדר כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#if !defined(PROGRAM_EXPORT)
#define PROGRAM_EXPORT /* NOTHING */

#if defined(WIN32) || defined(WIN64)
#undef PROGRAM_EXPORT
#if defined(PROGRAM_EXPORTS)
#define PROGRAM_EXPORT __declspec(dllexport)
#else
#define PROGRAM_EXPORT __declspec(dllimport)
#endif // defined(PROGRAM_EXPORTS)
#endif // defined(WIN32) || defined(WIN64)

#if defined(__GNUC__) || defined(__APPLE__) || defined(LINUX)
#if defined(PROGRAM_EXPORTS)
#undef PROGRAM_EXPORT
#define PROGRAM_EXPORT __attribute__((visibility("default")))
#endif // defined(PROGRAM_EXPORTS)
#endif // defined(__GNUC__) || defined(__APPLE__) || defined(LINUX)

#endif // !defined(PROGRAM_EXPORT)

הקוד הזה מגדיר מה מיוצא החוצה ומה נכנס.
עבור ווינדוס אנחנו צריכים לתת למחלקה את ההגדרה __declspec במידה ואנחנו מוציאים מה-dll צריך לשים:
dllexport.
במידה ואנחנו צורכים אותו, יש לציין dllimport.

שיו לב שהפרוייקט מוגדר בעזרת cmake.
כדי לבנות אותו יש לבצע את רצף הפקודות:

1
2
3
4
5
mkdir build
cd build
cmake ..
cmake --build .
cmake --open .

אם הינכם בווידוס וגם cmake מותקן וגם vs אז הוא ייפתח!

  • ניתן להשתמש גם ב-cmake-gui.

איך אנחנו נבדוק - ABI?
בווינדוס נבדוק בעזרת הכלי dumpbin שנותן לנו אופציה להתסכל לתוך binary outputs.

על הכלי ניתן לקרוא כאן:
https://learn.microsoft.com/en-us/cpp/build/reference/dumpbin-reference?view=msvc-170

הפקודה להרצה:

1
dumpbin.exe /EXPORTS ProgramLib.dll

במידה והכל עבד כראוי נראה:

1
2
3
4
5
ordinal hint RVA      name

1 0 00001000 ??4Calculator@@QEAAAEAV0@$$QEAV0@@Z
2 1 00001000 ??4Calculator@@QEAAAEAV0@AEBV0@@Z
3 2 00001010 ?Calculate@Calculator@@QEAAHHH@Z

השמות המוזרים האלו הם שמות מוערבבים על מנת לתת מזהה ייחודי לכל דבר שאנחנו מייחצנים על מנת שהם לא ייתנגשו אחד בשני.

חיבור לפייתון בעזרת cppyy

cppyy זו ספרייה שמייצרת בצורה אוטומטית את הגשרים בין קוד C++ לקוד פייתון.

כדי להתקין cppyy:

1
pip install cppyy

האתר שלהם:
https://cppyy.readthedocs.io/en/latest/

למי שרוצה לדעת יותר, הטכנולוגיה בנויה על גבי Cling:
https://github.com/vgvassilev/cling

החסרון העיקרי של הספריה הזו הוא כל התלויות של הספריה.
זה יוצר בעיתיות בלהתקין את הקוד שלנו בנקודות קצה מכיוון שאנחנו צריכים להתקין את כל התלויות.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import cppyy

# Copy programLib.hpp,programExport.hpp and programLib.dll to the root folder of PyCalculator

cppyy.include('programExport.hpp')
cppyy.include('programLib.hpp')
cppyy.load_library('programLib.dll')

from cppyy.gbl import Calculator

calc = Calculator()

result = calc.Calculate(2,3)
print(f'Result: {result}')

תעתיקו את הקבצים המצויינים לאותה תיקייה והריצו את קוד הפייתון!
אם פעלתם נכונה אתם תראו את התוצאה:

1
2
3
PyCalculator>py app.py
Hello from CPP
Result: 5

נכון מדהים?

קישוריות בין פייתון ל-.Net

ראינו כיצד מקשרים בין פייתון ל-cpp.
כעת נקשר בין פייתון לשפות .net כמו- c#

נשתמש ב-pythonnet.

כדי להתקין:

pip install pythonnet

מטרת pythonnet היא להביא את עולם הפייתון לעולם הדוטנט והפוך.

זהו מימוש פייתון ב-clr.

clr זה Common Language Runtime שהוא ה-Virtual Machine של שפות .Net.
שפות כגון - C#, VB, F#.

כדי לראות את הקוד תוכלו לצפות בתיקייה השלישית:

https://github.com/Ilya122/interoperability_learning_in_simplycode/tree/main/3_CLR_Python

ספריה ב-C#

NameGenerator Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class NameGenerator
{
private Random mRand = new Random();

private readonly IEnumerable<string> mNames = new string[] {"James" //.... מקוצר
};

public string GenerateName()
{
var randInd = mRand.Next(0, mNames.Count());

return mNames.ElementAt(randInd);
}
}

הקוד מבצע משהו מאוד פשוט - מייצר שם רנדומלי לאדם.

כדי לבנות את הקוד נשתמש ב-dotnet publish -o out_path

אך כדי לבנות את הקוד בשביל שנוכל להשתמש בו בפייתון נצטרך להוסיף ל-csproj שלנו את הקטע:

1
2
3
<PropertyGroup>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>

הקוד בפייתון

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import sys
import clr
from clr_loader import get_coreclr
from pythonnet import set_runtime

rt = get_coreclr(runtime_config=r'MyLib.runtimeconfig.json')
set_runtime(rt)

result = clr.FindAssembly("MyLib")
#print('FindAssembly returned:', result)
result = clr.AddReference("MyLib")
#print('AddReference returned:', result)

from MyLib import NameGenerator

generate = NameGenerator()

name = generate.GenerateName()

print(f'New name should be {name}')

שימו לב שלתיקייה העתקתי כבר את הקבצים הבינאריים, אתם יכולים לשחק עם זה ולהוסיף מתודות משלכם.
בזכות מה שהוספנו מקודם יש לנו קובץ לטעינה בשם - MyLib.runtimeconfig.json.

בהתחלה אנחנו טוענים את הקונפיגורציה ל-runtime.

1
2
rt = get_coreclr(runtime_config=r'MyLib.runtimeconfig.json')
set_runtime(rt)

הקבצים המיוצרים בדוטנט נקראים “אסבמליים” או Assembly.
כל קובץ dll יכול להכיל אסמבלי רבים או אחד.

כדי לטעון את האסמבלי אנחנו מבצעים:

1
result = clr.AddReference("MyLib")

ולבסוף נוכל להשתמש בקוד ה-c# בתוך פייתון!

1
2
3
4
5
6
7
from MyLib import NameGenerator

generate = NameGenerator()

name = generate.GenerateName()

print(f'New name should be {name}')

והתוצאה:

1
2
3
4
5
6
7
8
9
10
11
InteropLearning\3_CLR_Python\Py>py PrintName.py
New name should be Shirley

InteropLearning\3_CLR_Python\Py>py PrintName.py
New name should be Debra

InteropLearning\3_CLR_Python\Py>py PrintName.py
New name should be Matthew

InteropLearning\3_CLR_Python\Py>py PrintName.py
New name should be Nicholas

סקריפטים שעובדים בתוך ה-Runtime

למדנו על שירותים ב-HTTP.
למדנו אינטגרציה בין שפות כגון C# ו-פייתון.

כמו שפייתון היא שפה דינאמית וניתן בזמן ריצה להכניס קוד גם השפה -chaiscript היא שפה דינאמית המאפשרת ליצור אינטגרציה בין שפת סקריפט לשפה מקומפלת.

מה הסיבה שנרצה את זה בכלל?
בעזרת הטכניקה הזו ניתן לקחת חלק סטטי של המערכת ולהפוך אותה להיות מתוכנתת ודינאמית.

אחת הדוגמאות הבולטות מתחום המחשוב זה הכרטיס הגראפי.
כדי לקרוא יותר ניתן לקרוא מהפוסט למבוא:

מבוא לGLSL

Shader זהו סקריפט שניתן לטעון אותו ובאופן דינאמי לשנות איך התמונה מרונדרת למסך.

באופן דינאמי אנו טוענים טקסט שמייצג קוד Glsl.
הקוד משנה בצורה פשוטה את צבע התמונה.

באותו אופן נוכל לחבר שפת סקריפט למערכת שלנו כדי ליצור תת מערכת דינאמית.

Chaiscript and the Dynamic AI

לאתר של Chaiscript:
http://chaiscript.com/

בדוגמא הבאה אנחנו הולכים לייצר משחק ניחושים אבל הפוך.
אתם חושבים על מספר ולא אומרים למחשב מהו - והוא צריך לנחש מה המספר.

נפצל את המערכת לכמה מערכות:

  1. בחירת המספר
  2. ניחוש מספר על ידי המחשב
  3. בדיקה האם המספר נכון או לא
  4. המשך המשחק בהתאם לבחירת המחשב
  5. אם המחשב לא ניחש החזרת תשובה ע”י המשתמש אם המספר הוא גדול, קטן או שווה למספר.
  6. חזרה ל-2 אם המחשב טעה.
  7. נצחון אם המחשב צדק.

אנחנו ניקח את שלב 2 ונהפוך אותו לסקריפטאבילי - זאת אומרת שנכתוב אותו לא ב-C++ אלה בשפת סקריפט.
ככה שניתן להחליף את האלגוריתם בלי לשנות ולקמפל את המערכת.

הקוד

ניתן למצוא אותו כאן:

https://github.com/Ilya122/interoperability_learning_in_simplycode/tree/main/4_CPP_ChaiScript

אני לא ארחיב יותר מדי - אני רוצה שתקראו את הקוד ותנסו להבין אותו.

1
2
3
4
5
6
7
8
chaiscript::ChaiScript engine;
engine.add(chaiscript::fun(&IsItGreaterOrSmaller), "greater_or_smaller");
engine.add(chaiscript::fun(&MinNumber), "get_min_number");
engine.add(chaiscript::fun(&MaxNumber), "get_max_number");
engine.add_global(chaiscript::var(-1), "high");
engine.add_global(chaiscript::var(-1), "low");
engine.add_global(chaiscript::var(0), "current_guess");
auto boxedVal = engine.use("guessAIScript.chaiscript");

המערכת נותנת לשפת הסקריפט כלים להתמודד איתם בשביל לכתוב אלגוריתם.

  • greater_or_smaller נותנת אפשרות לשפת הסקריפט לבדוק את תוצאת המספר שניחש.
  • get_min_number ו -get_max_number כדי לדעת מהם גבולות המשחק.
  • 3 משתנים גלובאליים שיוכל לשמור בהם את החישובים שלהם.

בסופו אנחנו טוענים את הסקריפט שאנחנו הולכים להשתמש בו.

הסקריפט כתוב לאלגוריתם חיפוש בינארי שהולך לאמצע של כל דבר והוא זוכר מהי התוצאה הנמוכה ביותר שניחש ומהי הגבוה ביותר שניחש עד כה.

השפה עצמה בנויה כשפת ג’אווה-סקריפט והיא מאוד קלה לשימוש וללמידה.
ניתן להוסיף בה מגוון רחב של אובייקטים והיא עובדת עם C++ באופן מאוד צמוד.
היא כתובה ב-C++ מודרני!

כדי להשתמש בזה כל מה שאתם צריכים זה להוריד את הפרוייקט.
לבנות בעזרת Cmake:

1
2
3
4
cd 4_CPP_ChaiScript
mkdir build
cd build
cmake ..

ומפה אתם יכולים לקמפל את הפרוייקט ולהתנסות מולו.
שימו לב שקובץ הסקריפט צריך להיות ליד קובץ ההרצה כדי שיוכל לטעון אותו- במידה ואתם מדבגים.

סיכום

תאימות בין מערכות יכולה להיות ברמה הגלובאלית - איך אובייקטים נראים ובאילו פרוטוקולים הם משתמשים.
תאימות יכולה להיות גם בין קבצים בינאריים, טקסטואליים או כל דבר שאנחנו קוראים לו “מערכת”.

זוהי תכונה מאוד חשובה של תוכנה ופוגשת אותנו באינטגריות וכל מקום שאנחנו מנסים לשדרג ולחבר מערכות אחת לשנייה.
כלים כמו Http, Json עזרו לאינטרנט לחבר בקלות רבה מערכות רבות אחת לשנייה.
כיום מעדיפים כלים פשוטים שבכל שפה ניתן להשתמש בה וחשוב להכיר את האספקט הזה של התוכנה.

בעיצוב מערכת אנו צריכים לקחת בחשבון את כלל התכונות שאנחנו רוצים מהמוצר כולל תאימותו למערכות אחרות כי לא פעם נעבוד מול המתחרים שלנו יד ביד.

וכמובן - אל תשכחו לקרוא על הסטנדרט ולעמוד בו, אחרת זה ייעלה לכם הרבה כסף לתקן טעויות.

תודה על הקריאה!

על הפוסט

הפוסט נכתב על ידי Ilya, רישיון על ידי CC BY-NC-ND 4.0.

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#Software